// ==UserScript== // @name 2026新版人教培训全自动助手 // @namespace dclaw.pep.miaoxue // @version 3.5.1 // @description 人教网培(wp.pep.com.cn)一键全自动辅助:学习前自动同步平台进度(官方结论/模块进度/单课时长三重判定),已完成模块直接标✔跳过;自动收集、无人值守连播、后台防限速挂机(切标签页/最小化照学)、总进度与剩余时间预估、学完语音+弹窗报喜。真实1.0X播放,学时合规累计。 // @author DClaw // @match https://wp.pep.com.cn/* // @match https://*.gensee.com/* // @grant GM_setValue // @grant GM_getValue // @run-at document-idle // @license All Rights Reserved // ==/UserScript== (function () { 'use strict'; const IS_TOP = (window.top === window.self); const KEY = { queue: 'px_queue', running: 'px_running', speed: 'px_speed', listUrl: 'px_listUrl', active: 'px_active', autostart: 'px_autostart', welcomed: 'px_welcomed', doneDay: 'px_doneDay', tts: 'px_tts' }; const getQ = () => GM_getValue(KEY.queue, []); const setQ = q => GM_setValue(KEY.queue, q); const speed = () => Number(GM_getValue(KEY.speed, 1)); const running = () => !!GM_getValue(KEY.running, false); const sleep = ms => new Promise(r => setTimeout(r, ms)); const rnd = (a, b) => a + Math.random() * (b - a); const $ = id => document.getElementById(id); const LOGO = '🎓'; /* 收款码占位符:发布构建时替换为内嵌 data:image base64,勿改动字样 */ const QR_WX = '__PX_QR_WX__'; const QR_ZFB = '__PX_QR_ZFB__'; /* ================= 防挂机:伪造页面可见状态 ================= */ try { Object.defineProperty(document, 'visibilityState', { get: () => 'visible', configurable: true }); Object.defineProperty(document, 'hidden', { get: () => false, configurable: true }); const stop = e => e.stopImmediatePropagation(); ['visibilitychange', 'mozvisibilitychange', 'webkitvisibilitychange'].forEach(ev => document.addEventListener(ev, stop, true)); } catch (e) { /* 忽略 */ } /* ================= 后台挂机:静音音频防标签页限速 ================= */ /* 浏览器对“看不见且不出声”的标签页会把定时器降速到 1 分钟一次, 这段 0.0008 音量的循环音频人耳完全听不到,但让标签页被视为“正在播放”, 于是切去别的标签页、甚至最小化窗口,连播逻辑与看门狗照常全速运行。 */ let kaCtx = null, kaSrc = null; function bgKeepAlive(on) { try { if (on) { const Ctx = window.AudioContext || window.webkitAudioContext; if (!Ctx) return; kaCtx = kaCtx || new Ctx(); if (!kaSrc) { const len = Math.ceil(kaCtx.sampleRate * 0.5); const buf = kaCtx.createBuffer(1, len, kaCtx.sampleRate); // 全静音 buffer const osc = kaCtx.createOscillator(); const g = kaCtx.createGain(); g.gain.value = 0.0008; // 低于可闻阈值 osc.frequency.value = 20; // 20Hz,人耳听不见 osc.connect(g); g.connect(kaCtx.destination); osc.start(); kaSrc = osc; } if (kaCtx.state === 'suspended') kaCtx.resume().catch(() => {}); } else if (kaSrc) { try { kaSrc.stop(); } catch (_) {} kaSrc = null; } } catch (_) { /* 个别浏览器禁止自动播放,忽略即可 */ } } /* 用户第一次点页面时补一次解锁,防止 AudioContext 被autoplay策略挂起 */ document.addEventListener('pointerdown', () => { if (running() && kaCtx && kaCtx.state === 'suspended') kaCtx.resume().catch(() => {}); }, { passive: true }); /* ================= 语音播报 ================= */ function speak(text) { if (!GM_getValue(KEY.tts, 1)) return; try { const u = new SpeechSynthesisUtterance(text); u.lang = 'zh-CN'; u.rate = 0.95; u.volume = 1; speechSynthesis.cancel(); speechSynthesis.speak(u); } catch (_) { /* 无语音引擎时静默 */ } } /* ================= 全部学完:弹窗报喜 + 语音 ================= */ function celebrate() { if (!document.body || $('px-win')) return; const mask = document.createElement('div'); mask.className = 'px-mask'; mask.id = 'px-win'; mask.innerHTML = `
🎉 恭喜!全部课程已学完本培训任务的视频部分,通关
今晚可以早睡啦 🌙
学习数据 T+1 更新,明天记得打开「学习数据」页核对;
未达 100% 的模块,脚本会自动重新排队补学。
`; document.body.appendChild(mask); $('px-win-ok').onclick = () => mask.remove(); speak('恭喜,本次培训全部课程已经学习完成,明天记得查看学习数据'); setTimeout(() => { const m = $('px-win'); if (m) m.remove(); }, 20000); } /* ================= 样式 ================= */ const style = document.createElement('style'); style.textContent = ` @keyframes px-pop{from{opacity:0;transform:translateY(14px) scale(.96);}to{opacity:1;transform:none;}} .px-btn{background:linear-gradient(135deg,#6c5ce7,#4834d4);border:none;color:#fff;border-radius:9px; padding:7px 14px;cursor:pointer;font:600 13px/1.2 "Microsoft YaHei",sans-serif;transition:all .2s; box-shadow:0 2px 10px rgba(108,92,231,.35);} .px-btn:hover{transform:translateY(-1px);filter:brightness(1.08);} .px-btn.gray{background:#eef1f3;color:#667;box-shadow:none;} .px-btn.gray:hover{background:#e2e6e9;} .px-btn.big{width:100%;padding:12px;font-size:15px;letter-spacing:2px;border-radius:10px; background:linear-gradient(135deg,#11998e,#38ef7d);box-shadow:0 4px 18px rgba(17,153,142,.45);} #px-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:320px; background:rgba(255,255,255,.94);backdrop-filter:blur(12px);border-radius:16px; box-shadow:0 10px 40px rgba(20,20,40,.22);font:13px/1.7 "Microsoft YaHei",sans-serif;color:#333; overflow:hidden;animation:px-pop .35s ease;} #px-panel .px-head{background:linear-gradient(135deg,#11998e,#0f6f8f);color:#fff;padding:11px 14px; display:flex;justify-content:space-between;align-items:center;font-weight:bold;font-size:14px;cursor:move;} #px-panel .px-head .t{display:flex;align-items:center;gap:7px;} #px-panel .px-head .t i{font-style:normal;font-size:17px;} #px-panel .px-head .t .ver{font-size:10px;opacity:.8;font-weight:normal;} #px-panel .px-head b{cursor:pointer;font-weight:normal;padding:4px 9px;opacity:.92;font-size:12px; border-radius:99px;background:rgba(255,255,255,.16);user-select:none;white-space:nowrap;} #px-panel .px-head b:hover{opacity:1;background:rgba(255,255,255,.3);} #px-panel .px-body{padding:12px 14px;} #px-panel .px-row{margin:7px 0;display:flex;align-items:center;gap:8px;flex-wrap:wrap;} #px-panel .px-tool{display:flex;gap:6px;margin-top:9px;} #px-panel .px-tool b{flex:1;text-align:center;cursor:pointer;font-weight:600;font-size:12px;padding:6px 0; border-radius:9px;background:#eef6f4;color:#0f6f8f;border:1px solid #dcebe8;user-select:none; white-space:nowrap;transition:all .15s;} #px-panel .px-tool b:hover{background:#ddf0eb;border-color:#c4e0d9;} #px-panel .px-prog{background:#f7fbfa;border:1px solid #e4efec;border-radius:11px;padding:8px 11px;margin:9px 0;} #px-panel select{border:1px solid #d8dee3;border-radius:6px;padding:3px 6px;font:13px "Microsoft YaHei";background:#fff;} #px-progbar{height:8px;background:#eef1f3;border-radius:99px;overflow:hidden;flex:1;} #px-progbar i{display:block;height:100%;background:linear-gradient(90deg,#11998e,#38ef7d);width:0;transition:width .6s;} .px-stat{font-size:12px;color:#556;} .px-warn{color:#8a6d3b;font-size:11.5px;background:#fff8e6;border-radius:6px;padding:4px 8px;} /* 已学完标签(列表页) */ .px-done-badge{display:inline-block;background:linear-gradient(135deg,#11998e,#38ef7d);color:#fff; border-radius:99px;padding:2px 10px;font-size:11px;margin-left:6px;vertical-align:middle; box-shadow:0 2px 6px rgba(17,153,142,.4);animation:px-pop .3s ease;} /* 队列抽屉 */ .px-qlist{max-height:190px;overflow:auto;border:1px solid #edf0f2;border-radius:10px;margin:6px 0; background:#fbfdfd;scrollbar-width:thin;} .px-qlist::-webkit-scrollbar{width:5px;} .px-qlist::-webkit-scrollbar-thumb{background:#cfe; border-radius:99px;} .px-qi{padding:5px 9px;border-bottom:1px dashed #f0f2f4;font-size:12px;cursor:pointer;display:flex;gap:6px;align-items:center;} .px-qi:hover{background:#eefaf5;} .px-qi.done{color:#17b978;cursor:default;} .px-qi.done:hover{background:none;} .px-qi.cur{color:#0f6f8f;font-weight:bold;} .px-qi em{font-style:normal;color:#aab;font-size:11px;margin-left:auto;flex:none;} /* 打赏支持区 */ .px-sup{margin-top:8px;padding-top:10px;text-align:center;border-radius:10px;} .px-sup-border{border-top:1px dashed #dfe6ea;} .px-sup h4{margin:0 0 4px;font-size:13px;color:#333;font-weight:bold;} .px-sup p{margin:2px 0 8px;font-size:11.5px;color:#778;line-height:1.7;} .px-qrs{display:flex;justify-content:center;gap:14px;margin:8px 0;flex-wrap:wrap;} .px-qrs figure{margin:0;} .px-qrs img{width:112px;height:112px;object-fit:contain;border-radius:10px;border:1px solid #eee;background:#fff; cursor:zoom-in;transition:all .2s;box-shadow:0 2px 8px rgba(0,0,0,.07);} .px-qrs img:hover{transform:scale(1.08) translateY(-2px);box-shadow:0 8px 20px rgba(0,0,0,.18);} .px-qrs figcaption{font-size:11.5px;color:#667;margin-top:4px;font-weight:bold;} .px-qrs figcaption small{display:block;font-weight:normal;color:#99a;font-size:10.5px;} a.px-qq{display:inline-block;margin-top:4px;color:#0f6f8f;font-size:12px;font-weight:bold;} /* 扫码灯箱 */ #px-light{position:fixed;inset:0;background:rgba(10,10,25,.86);z-index:2147483647;display:flex; align-items:center;justify-content:center;cursor:zoom-out;animation:px-pop .25s ease;} #px-light .card{background:#fff;border-radius:18px;padding:22px 26px 16px;text-align:center; font-family:"Microsoft YaHei",sans-serif;box-shadow:0 20px 80px rgba(0,0,0,.5);} #px-light img{width:min(430px,72vw);height:min(430px,60vh);object-fit:contain;border-radius:8px;} #px-light h3{margin:10px 0 2px;font-size:16px;color:#333;} #px-light p{margin:0;font-size:12px;color:#889;} #px-light .x{position:absolute;top:22px;right:30px;color:#fff;font-size:30px;cursor:pointer;} /* 弹窗 */ .px-mask{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:2147483646;display:flex; align-items:center;justify-content:center;font-family:"Microsoft YaHei",sans-serif;} .px-modal{width:540px;max-width:92vw;max-height:88vh;overflow:auto;background:#fff;border-radius:18px; box-shadow:0 16px 60px rgba(0,0,0,.3);animation:px-pop .3s ease;} .px-modal .m-head{background:linear-gradient(135deg,#11998e,#0f6f8f);color:#fff;padding:20px 26px; border-radius:18px 18px 0 0;font-size:19px;font-weight:bold;} .px-modal .m-head small{display:block;font-size:12px;font-weight:normal;opacity:.9;margin-top:4px;} .px-modal .m-body{padding:18px 26px;font-size:13.5px;color:#445;line-height:1.9;} .px-modal .m-body ol{margin:6px 0;padding-left:20px;} .px-modal .m-body h5{margin:14px 0 4px;color:#0f6f8f;} .px-modal .m-foot{padding:0 26px 22px;text-align:center;} `; (document.head || document.documentElement).appendChild(style); /* ================= 扫码灯箱 ================= */ function showQR(src, name) { const old = $('px-light'); if (old) old.remove(); const m = document.createElement('div'); m.id = 'px-light'; m.innerHTML = `
${name}

${name} 收款码

金额随意 · 纯属心意 | 屏幕扫码或长按截图后,用${name}扫一扫

`; m.onclick = () => m.remove(); document.body.appendChild(m); } /* ================= 打赏 & 反馈卡片 ================= */ function buildSupport(inModal) { const box = document.createElement('div'); box.id = inModal ? 'px-support-modal' : 'px-support'; box.className = inModal ? 'px-sup' : 'px-sup px-sup-border'; box.innerHTML = `

☕ 如果它替您守住了一个夜晚

白天站讲台,晚上补学时——做这个脚本的自己也是这么熬过来的。
脚本永远免费、无广告、不碰账号密码。若它让您今晚早睡了一小时,
请作者喝杯咖啡就好:金额随意,哪怕一块钱的鼓励,也会变成群里第一时间更新的新版本。

微信点击放大 · 可直接扫码
支付宝点击放大 · 可直接扫码
🐧 报错 / 催更 / 抄作业:点击加入群聊【脚本反馈问题】→`; box.querySelectorAll('.px-qrs img').forEach(im => { im.onclick = () => showQR(im.src, im.dataset.name); }); return box; } /* ================= 使用教程弹窗 ================= */ function showHelp() { if ($('px-help')) return; const mask = document.createElement('div'); mask.className = 'px-mask'; mask.id = 'px-help'; mask.innerHTML = `
${LOGO} 2026新版人教培训全自动助手 · 使用说明把"盯着播放键"这件事交给脚本,把夜晚还给老师
📌 两步上手(真的只要两步)
  1. 多学科只收集一次:在「课程学习」页点「收集课程」,切学科下拉框再点一次,队列自动去重合并;
  2. 之后每天只需点绿色大按钮「🚀 一键全自动」——自动核对学习数据、自动跳过已学完、自动连播、播完自动下一课、学完自动收工回列表。列表页每门课后面会打上 ✔ 已学完 标签,一眼看清进度。
📡 自动跳过已学完
每次点「一键全自动」,脚本先向平台拉取学习数据与各模块学时明细(已学/总时长):平台记录已看完的课自动打 ✔ 跳过,只播真正没学完的;待学课还会显示“还差 xx:xx”。数据为 T+1,当天刚学完的可能要明天才被平台确认跳过。
🧍 真人节奏
进课先随机停顿几秒再开播,课与课之间随机休息 8~25 秒——像一位真坐在屏幕前的老师,而不是机器人。全程真实 1.0X 播放,学时由服务端如实累计,第二天「学习数据」自动更新。
🌙 后台挂机 · 不占用浏览器
点完「一键全自动」后,您可以切去别的标签页备课、改作业,甚至最小化窗口——脚本用一段人耳听不见的静音音频保住标签页不被浏览器降速,连播与恢复逻辑后台照常全速运行。面板上 📈 一行实时显示总进度百分比与剩余时长预估,收起成悬浮球后悬停也能看到进度。
🎉 学完报喜
全部视频播完时,屏幕弹出通关庆祝窗,并用语音播报「恭喜,本次培训全部课程已经学习完成」(嫌吵可点面板上方「🔊 播报」关闭,仅留文字弹窗)。
⚠️ 注意事项
平台培训须知明确“请勿使用倍速播放”,脚本默认 1.0X;倍速档仅作应急,存在学时不被认可风险。一个账号同一时间只挂一个页面;补学截止前留 1~2 天缓冲核对。
`; document.body.appendChild(mask); $('px-modal-support').appendChild(buildSupport(true)); const btn = document.createElement('button'); btn.className = 'px-btn big'; btn.style.marginTop = '12px'; btn.textContent = '我已了解,开始开挂(合规版)🚀'; btn.onclick = () => { GM_setValue(KEY.welcomed, 1); mask.remove(); }; $('px-modal-support').appendChild(btn); } /* ================= 主面板 ================= */ let supportOpen = false, queueOpen = false; function buildPanel({ onList = false, onData = false } = {}) { if (!IS_TOP || $('px-panel') || !document.body) return; const p = document.createElement('div'); p.id = 'px-panel'; p.innerHTML = `
${LOGO}人教培训全自动助手 2026·v3 - 收起
📋 队列 🔊 播报 ❔ 教程
${onList || onData ? `
${onList ? `` : ''}${onData ? `` : ''}
` : ''}
⚡ 倍速
⚠ 建议保持 1.0X(平台须知勿倍速);学习数据次日更新
🌙 点一键后可切走标签页、最小化窗口,后台照学不断速
💖 支持作者 & 反馈群
`; document.body.appendChild(p); $('px-speed').value = String(GM_getValue(KEY.speed, 1)); $('px-speed').onchange = e => { GM_setValue(KEY.speed, Number(e.target.value)); const v = findVideo(); if (v) { try { v.playbackRate = speed(); } catch (_) {} } }; $('px-min').onclick = () => { p.style.display = 'none'; showFab(true); }; $('px-help-btn').onclick = showHelp; $('px-stop').onclick = () => { GM_setValue(KEY.running, false); refreshStat(); }; $('px-q').onclick = () => { queueOpen = !queueOpen; renderQueue(); }; const paintTts = () => { const b = $('px-tts'); if (b) b.textContent = GM_getValue(KEY.tts, 1) ? '🔊 播报' : '🔇 已关'; }; $('px-tts').onclick = () => { GM_setValue(KEY.tts, GM_getValue(KEY.tts, 1) ? 0 : 1); paintTts(); }; paintTts(); $('px-sup-toggle').onclick = () => { supportOpen = !supportOpen; $('px-support-slot').innerHTML = ''; if (supportOpen) $('px-support-slot').appendChild(buildSupport()); }; $('px-auto').onclick = onAuto; if (onList) $('px-collect').onclick = () => collect(false); if (onData) $('px-check').onclick = () => checkData(false); refreshStat(); if (!GM_getValue(KEY.welcomed, 0) && (onList || onData)) setTimeout(showHelp, 600); if (onList && GM_getValue(KEY.autostart, 0)) { GM_setValue(KEY.autostart, 0); setTimeout(onAuto, 800); } if (onList) { paintBadges(); setInterval(paintBadges, 2500); } // 进入页面即先行同步:昨天/之前学完的模块当场标 ✔,不待点一键就不学 if (onList && getQ().length) setTimeout(() => autoSync(false), 1200); if (onData) setTimeout(() => autoSync(false), 1200); // 拖拽 const head = p.querySelector('.px-head'); head.addEventListener('mousedown', ev => { if (ev.target.tagName === 'B') return; const sx = ev.clientX - p.offsetLeft, sy = ev.clientY - p.offsetTop; const mv = e => { p.style.left = (e.clientX - sx) + 'px'; p.style.top = (e.clientY - sy) + 'px'; p.style.right = 'auto'; p.style.bottom = 'auto'; }; const up = () => { document.removeEventListener('mousemove', mv); document.removeEventListener('mouseup', up); }; document.addEventListener('mousemove', mv); document.addEventListener('mouseup', up); ev.preventDefault(); }); } /* ================= 队列抽屉 ================= */ function renderQueue() { const box = $('px-queuebox'); if (!box) return; if (!queueOpen) { box.innerHTML = ''; return; } const q = getQ(); const act = GM_getValue(KEY.active, ''); if (!q.length) { box.innerHTML = '
队列空空,先去「课程学习」页点「收集课程」
'; return; } box.innerHTML = '
'; const list = box.firstElementChild; q.forEach(c => { const d = document.createElement('div'); d.className = 'px-qi' + (c.done ? ' done' : (c.id === act && running() ? ' cur' : '')); const tag = c.done ? (c.serverDone ? '✔ 已学完(平台确认)' : '✔ 已学完') : (c.id === act && running() ? '▶ 正在学' : '· 待学'); d.innerHTML = `${c.done ? '✔' : (c.id === act && running() ? '▶' : '·')}${c.title}${c.module || ''}${!c.done && c.remain ? ' 还差' + fmtDur(c.remain) : ''}`; d.title = tag; if (!c.done) d.onclick = () => { GM_setValue(KEY.running, true); GM_setValue(KEY.active, c.id); location.href = c.url; }; list.appendChild(d); }); } /* ================= 列表页:已学完打标 ================= */ function paintBadges() { const done = {}; getQ().forEach(c => done[c.id] = c.done); document.querySelectorAll('a[href*="/px/entryRoom/"]').forEach(a => { const m = a.href.match(/entryRoom\/\d+\/(\d+)\//); if (!m) return; const par = a.parentElement; if (!par) return; let badge = par.querySelector('.px-done-badge[data-for="' + m[1] + '"]'); if (done[m[1]] && !badge) { badge = document.createElement('span'); badge.className = 'px-done-badge'; badge.dataset.for = m[1]; badge.textContent = '✔ 已学完'; a.insertAdjacentElement('afterend', badge); } else if (!done[m[1]] && badge) { badge.remove(); } }); } function showFab(on) { let f = $('px-fab'); if (!f) { f = document.createElement('div'); f.id = 'px-fab'; f.textContent = LOGO; f.title = '🎓 2026人教培训全自动助手:点击重新展开面板'; f.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:2147483646;width:48px;height:48px;' + 'border-radius:50%;background:linear-gradient(135deg,#11998e,#0f6f8f);color:#fff;font-size:22px;' + 'line-height:48px;text-align:center;cursor:pointer;box-shadow:0 6px 20px rgba(17,153,142,.5);' + 'transition:transform .2s;'; f.onmouseenter = () => f.style.transform = 'scale(1.12)'; f.onmouseleave = () => f.style.transform = ''; f.onclick = () => { f.style.display = 'none'; const p = $('px-panel'); if (p) p.style.display = ''; }; document.body.appendChild(f); } f.style.display = on ? 'block' : 'none'; } function refreshStat() { bgKeepAlive(running()); // 连播中就挂静音保活,切走/最小化不降速 const el = $('px-stat'), fr = $('px-frac'), bar = $('px-progbar i'), eta = $('px-eta'); if (!el) return; const q = getQ(), done = q.filter(c => c.done).length; const rec = GM_getValue(KEY.doneDay, { d: '', n: 0 }); const today = new Date(); const tkey = today.getFullYear() + '-' + (today.getMonth() + 1) + '-' + today.getDate(); const tn = rec.d === tkey ? rec.n : 0; el.innerHTML = running() ? '🟢 连播中 · 今日完成 ' + tn + ' 门 | 🌙 可切走页面' : (q.length ? '⏸ 待机 · 今日完成 ' + tn + ' 门' : '队列空空,先去课程页点「收集课程」'); const pct = q.length ? Math.round(done / q.length * 100) : 0; if (fr) fr.textContent = q.length ? `${done}/${q.length} · ${pct}%` : ''; if (bar) bar.style.width = pct + '%'; if (eta) { if (!q.length) eta.textContent = ''; else if (pct >= 100) eta.textContent = '🏁 全部学完!明天核对「学习数据」即可'; else { const left = q.filter(c => !c.done); const known = left.filter(c => c.remain !== undefined); const sec = known.reduce((s, c) => s + c.remain, 0); if (known.length === left.length && sec > 0) { const h = Math.floor(sec / 3600), m = Math.round(sec % 3600 / 60); eta.textContent = '📈 总进度 ' + pct + '% | 1.0X 播放预计还需约 ' + (h ? h + ' 小时 ' : '') + m + ' 分钟'; } else { eta.textContent = '📈 总进度 ' + pct + '%(剩余时长由平台进度同步得出,点「一键全自动」前先自动同步)'; } } } const f = $('px-fab'); if (f) f.title = q.length ? '🎓 2026人教培训全自动助手:进度 ' + done + '/' + q.length + '(' + pct + '%)' + (running() ? ' · 🟢 连播中' : ' · ⏸ 待机') + '|点击展开面板' : '🎓 2026人教培训全自动助手:点击展开面板'; if (queueOpen) renderQueue(); } setInterval(() => { if (IS_TOP) refreshStat(); }, 3000); /* ================= 今日完成计数 ================= */ function markDone(id) { const q = getQ(); const c = q.find(x => x.id === id); if (!c || c.done) return; c.done = true; setQ(q); const t = new Date(); const tkey = t.getFullYear() + '-' + (t.getMonth() + 1) + '-' + t.getDate(); const rec = GM_getValue(KEY.doneDay, { d: '', n: 0 }); GM_setValue(KEY.doneDay, rec.d === tkey ? { d: tkey, n: rec.n + 1 } : { d: tkey, n: 1 }); } /* ================= 同步平台进度:已学完自动跳过 ================= */ function toSec(t) { const p = String(t || '').split(':').map(Number); return p.length === 3 ? p[0] * 3600 + p[1] * 60 + p[2] : p.length === 2 ? p[0] * 60 + p[1] : 0; } const fmtDur = s => { s = Math.max(0, Math.round(s)); const h = (s / 3600) | 0, m = (s % 3600 / 60) | 0, x = s % 60; return (h ? h + ':' : '') + String(m).padStart(2, '0') + ':' + String(x).padStart(2, '0'); }; const norm = s => String(s || '').replace(/\s+/g, ''); async function fetchDoc(url) { const res = await fetch(url, { credentials: 'include' }); return new DOMParser().parseFromString(await res.text(), 'text/html'); } async function syncFromServer() { const src = GM_getValue(KEY.listUrl, '') + ' ' + location.href; const pid = (src.match(/(?:showStu|px\/index)\/(\d+)/) || [])[1] || '196'; const doc = await fetchDoc('https://wp.pep.com.cn/web/index.php?/user/showStu/' + pid); const mods = {}; // 模块名 → {pct, href} doc.querySelectorAll('a[href*="showStuOne"]').forEach(a => { const mm = a.href.match(/showStuOne\/\d+\/(\d+)\/(\d)/); if (!mm || mm[2] !== '1') return; // 只处理必学 const tr = a.closest('tr'); if (!tr) return; const td = tr.querySelector('td'); if (!td) return; const pm = tr.textContent.match(/(\d+)\s*%/); mods[norm(td.textContent)] = { pct: pm ? Number(pm[1]) : 0, href: a.href }; }); // 官方结论文案(最高优先级判定):“××必学,已达到学时要求。” / “××必学,请再学习01:48:23。” try { const full = doc.body.textContent; let m2; const reDone = /([一-龥、0-9]{2,15}?),已达到学时要求/g; while ((m2 = reDone.exec(full))) { const k = norm(m2[1]); if (mods[k]) { mods[k].pct = 100; mods[k].official = true; } else mods[k] = { pct: 100, href: '', official: true }; } const reRem = /([一-龥、0-9]{2,15}?),请再学习(\d{1,2}:\d{2}:\d{2})/g; while ((m2 = reRem.exec(full))) { const k = norm(m2[1]); if (mods[k]) { mods[k].pct = Math.min(mods[k].pct, 99); mods[k].need = toSec(m2[2]); } } } catch (_) { /* 文案结构变动不致命 */ } const q = getQ(); let skipped = 0; // 1) 模块整体 100% → 该模块全部课程直接标完成 q.forEach(c => { const info = mods[norm(c.module || '')]; if (!c.done && info && info.pct >= 100) { c.done = true; c.serverDone = true; skipped++; } }); // 2) 未完成模块 → 逐课读取“已学/总时长”明细,单课达标即跳过 const byMod = {}; q.forEach(c => { if (!c.done && c.module) (byMod[norm(c.module)] = byMod[norm(c.module)] || []).push(c); }); for (const mod of Object.keys(byMod)) { const info = mods[mod]; if (!info || !info.href) continue; let ddoc; try { ddoc = await fetchDoc(info.href); } catch (_) { continue; } const rows = []; ddoc.querySelectorAll('tr').forEach(tr => { const tm = tr.textContent.match(/(\d{1,2}:\d{2}:\d{2})\s*\/\s*(\d{1,2}:\d{2}:\d{2})/); if (!tm) return; const td = tr.querySelector('td'); if (!td) return; rows.push({ name: norm(td.textContent), watched: toSec(tm[1]), total: toSec(tm[2]) }); }); if (!rows.length) continue; const cs = byMod[mod]; const used = new Set(); const pair = rows.length === cs.length ? cs.map((c, i) => rows[i]) // 数量一致:按顺序配对 : cs.map(c => { // 不一致:按课名+出现次序配对 const i = rows.findIndex((r, j) => !used.has(j) && (r.name.slice(0, 12) === norm(c.title).slice(0, 12) || r.name.startsWith(norm(c.title)))); if (i >= 0) used.add(i); return i >= 0 ? rows[i] : null; }); cs.forEach((c, i) => { const r = pair[i]; if (!r || !r.total) return; c.remain = Math.max(0, r.total - r.watched); if (r.watched / r.total >= 0.99 || c.remain <= 15) { // 看完达标 → 跳过 c.done = true; c.serverDone = true; skipped++; } }); // 明细对不上号的课:退而用模块级“请再学习X”剩余时间展示 if (info.need) cs.forEach(c => { if (c.remain === undefined) c.remain = info.need; }); } setQ(q); return { skipped, mods }; } /* 静默同步:拉取平台学习进度,已完成模块/课程自动标 ✔ 跳过 */ function autoSync(showZero) { return syncFromServer().then(r => { if (r.skipped) toast('✔ 已同步平台进度:' + r.skipped + ' 门已完成课程自动标 ✔,不再学习'); else if (showZero) toast('✔ 平台进度已同步,无已完成课程'); paintBadges(); refreshStat(); return r; }).catch(() => { /* 同步失败静默忽略,不打扰学习流程 */ }); } /* ================= 核心动作 ================= */ function collect(silent) { const q = getQ(); let added = 0, currentModule = ''; document.querySelectorAll('tr').forEach(tr => { const h = tr.querySelector('h4'); // 绿色模块标题,如“一年级必学” if (h) { const t = h.textContent.trim(); if (t && !/公告|须知/.test(t)) currentModule = t; } tr.querySelectorAll('a[href*="/px/entryRoom/"]').forEach(a => { const m = a.href.match(/entryRoom\/\d+\/(\d+)\//); if (!m) return; const img = a.querySelector('img'); if (img && /btn_jrpx/.test(img.src)) return; // 选学点播不计学时,默认不收 if (q.some(c => c.id === m[1])) return; const title = (tr.querySelector('h6') || {}).textContent || ''; q.push({ id: m[1], title: title.trim().slice(0, 40) || m[1], module: currentModule, url: a.href, done: false }); added++; }); }); setQ(q); GM_setValue(KEY.listUrl, location.href); if (!silent) { alert(`已收集 ${added} 门(队列共 ${q.filter(c => !c.done).length} 门待学)。\n正在自动同步平台进度,已学完的将直接标 ✔ 跳过…`); autoSync(false); } return added; } function startQueue() { const next = getQ().find(c => !c.done); if (!next) { GM_setValue(KEY.running, false); bgKeepAlive(false); celebrate(); return; } GM_setValue(KEY.running, true); bgKeepAlive(true); GM_setValue(KEY.active, next.id); location.href = next.url; } async function onAuto() { toast('📡 同步平台进度,已学完的课程将自动跳过…'); try { const r = await syncFromServer(); if (r.skipped) toast('✔ 自动跳过已学完的 ' + r.skipped + ' 门课'); else if (getQ().length) toast('平台进度已同步,开始学习'); } catch (e) { /* 同步失败不阻塞,按本地队列继续 */ } if (/\/px\/index/.test(location.href)) { if (!getQ().some(c => !c.done)) collect(true); } else if (/\/user\/showStu/.test(location.href)) { checkData(true); } startQueue(); } function checkData(silent) { const pend = []; document.querySelectorAll('a[href*="showStuOne"]').forEach(a => { const m = a.href.match(/showStuOne\/\d+\/(\d+)\/(\d)/); if (!m || m[2] !== '1') return; const tr = a.closest('tr'); if (!tr) return; const td = tr.querySelector('td'); if (!td) return; const pm = tr.textContent.match(/(\d+)\s*%/); const pct = pm ? Number(pm[1]) : 0; if (pct < 100) pend.push({ name: td.textContent.trim(), pct }); }); const q = getQ(); if (!pend.length) { if (!silent) alert('查看到的必学模块已全部 100%!🎉\n(学习数据次日更新,今天学的明天才见效)'); return; } let added = 0; const miss = []; pend.forEach(p => { const cs = q.filter(c => c.module === p.name); if (!cs.length) { miss.push(p.name); return; } // 平台已确认完成的课不回炉;仅回炉“本地标记完成但平台未确认”的(通常是昨日新学的) cs.forEach(c => { if (c.done && !c.serverDone) { c.done = false; added++; } }); }); setQ(q); if (!silent) { if (miss.length) { GM_setValue(KEY.autostart, 1); alert(`以下未达标模块还没收集过课程:${miss.join('、')}\n将自动跳转课程页收集并开始学习。`); location.href = GM_getValue(KEY.listUrl, 'https://wp.pep.com.cn/web/index.php?/px/index/196'); } else { alert(`未达标模块:${pend.map(p => `${p.name} ${p.pct}%`).join(';')}\n已重排 ${added} 门课,点「🚀 一键全自动」继续。`); } } } /* ================= 播放器接管(gensee) ================= */ function findVideo() { let v = document.querySelector('video'); if (!v) for (const f of document.querySelectorAll('iframe')) { try { v = f.contentDocument && f.contentDocument.querySelector('video'); if (v) break; } catch (_) {} } return v; } function toast(msg) { if (!document.body) return; const t = document.createElement('div'); t.textContent = msg; t.style.cssText = 'position:fixed;left:50%;top:18%;transform:translateX(-50%);z-index:2147483647;' + 'background:rgba(10,10,25,.82);color:#fff;padding:12px 28px;border-radius:99px;font:15px "Microsoft YaHei";' + 'box-shadow:0 6px 24px rgba(0,0,0,.35);transition:opacity .6s;animation:px-pop .3s ease;'; document.body.appendChild(t); setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 800); }, 3800); } function attachPlayer(v) { // 防呆:若连播中打开的这门课其实已学完(平台确认),直接跳过 if (running()) { const cur = getQ().find(x => x.id === GM_getValue(KEY.active, '')); if (cur && cur.done && cur.serverDone) { const next = getQ().find(c => !c.done); toast('本课平台已记录完成,自动跳过 →'); setTimeout(() => { if (next) { GM_setValue(KEY.active, next.id); location.href = next.url; } else { GM_setValue(KEY.running, false); bgKeepAlive(false); celebrate(); } }, rnd(2000, 4000)); return; } } v.muted = true; if (running()) bgKeepAlive(true); // 播放页立刻挂保活,不等面板轮询 const apply = () => { try { v.playbackRate = speed(); } catch (_) {} }; const play = () => { v.play().catch(() => { v.muted = true; v.play().catch(() => {}); }); apply(); }; let started = false; if (running()) { // 真人节奏:进页随机停 3~8 秒再“点下播放” setTimeout(() => { started = true; play(); }, rnd(3000, 8000)); } apply(); v.addEventListener('ratechange', apply); v.addEventListener('pause', () => { if (running() && started) setTimeout(play, 800); }); let ended = false; v.addEventListener('ended', () => { if (ended) return; ended = true; markDone(GM_getValue(KEY.active, '')); if (!running()) return; const q = getQ(); const next = q.find(c => !c.done); if (!next) { GM_setValue(KEY.running, false); bgKeepAlive(false); celebrate(); // 弹窗 + 语音报喜 setTimeout(() => location.href = GM_getValue(KEY.listUrl, 'https://wp.pep.com.cn/web/index.php?/px/index/196'), 8000); return; } const rest = Math.round(rnd(8, 25)); // 课间随机休息 8~25 秒,模拟真人 toast(`本门已学完 ✔ ${rest} 秒后自动进入下一课`); setTimeout(() => { GM_setValue(KEY.active, next.id); location.href = next.url; }, rest * 1000); }); // 看门狗:卡住/暂停自动恢复 let lastT = v.currentTime, stuck = 0; setInterval(() => { if (ended || !running() || !started) return; if (v.paused) { play(); stuck = 0; return; } if (Math.abs(v.currentTime - lastT) < 0.1) { stuck++; if (stuck >= 4) { try { v.currentTime += 1; } catch (_) {} play(); stuck = 0; } } else { lastT = v.currentTime; stuck = 0; } apply(); }, 3000); } async function boot() { const isWp = /wp\.pep\.com\.cn/.test(location.host); if (isWp) { const waitBody = setInterval(() => { if (!document.body) return; clearInterval(waitBody); buildPanel({ onList: /\/px\/index/.test(location.href), onData: /\/user\/showStu/.test(location.href) }); }, 500); } else if (/gensee/.test(location.host)) { for (let i = 0; i < 90; i++) { const v = findVideo(); if (v) { attachPlayer(v); return; } await sleep(1000); } if (running()) location.reload(); // 90 秒没找到播放器,自动刷新重试 } } // 手动点“观看回放”时也记录当前课程 id document.addEventListener('click', e => { const a = e.target.closest && e.target.closest('a[href*="/px/entryRoom/"]'); if (a) { const m = a.href.match(/entryRoom\/\d+\/(\d+)\//); if (m) GM_setValue(KEY.active, m[1]); } }, true); boot(); })();