// ==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}扫一扫
`;
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新版人教培训全自动助手 · 使用说明把"盯着播放键"这件事交给脚本,把夜晚还给老师
📌 两步上手(真的只要两步)
- 多学科只收集一次:在「课程学习」页点「收集课程」,切学科下拉框再点一次,队列自动去重合并;
- 之后每天只需点绿色大按钮「🚀 一键全自动」——自动核对学习数据、自动跳过已学完、自动连播、播完自动下一课、学完自动收工回列表。列表页每门课后面会打上 ✔ 已学完 标签,一眼看清进度。
📡 自动跳过已学完
每次点「一键全自动」,脚本先向平台拉取学习数据与各模块学时明细(
已学/总时长):平台记录已看完的课自动打 ✔ 跳过,只播真正没学完的;待学课还会显示“还差 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();
})();