功能说明
每年继续教育要求视频学习,并在线考试。但是每15分钟会弹出提示框,点确定后才能及时播放视频计时,有时候一忙就忘了,这个工具可用模拟鼠标自动点确定继续播放,解放双手,提高效率。
适用于河南专技在线(会计人员)继续教育网络学习平台(网址:https://hnkj.ghlearning.com)。
使用步骤:
- 复制上面全部代码 → 打开 Tampermonkey → 找到旧脚本 → 整页替换 → Ctrl+S 保存。
- 刷新学习页面,什么都不用动,继续挂机。
- 等 15 分钟弹窗出现,观察控制台:
- [AutoClose] xx:xx:xx | success | DOM-15min 点击成功,弹窗已关闭
- 出现即代表真鼠标点击生效,视频会继续播放。
// ==UserScript==
// @name 河南科技-自动关弹窗(30秒慢速版)
// @namespace https://github/xxx
// @version 5.0
// @description 30秒扫一次;DOM+图片双兜底;原生鼠标点击
// @author You
// @match https://hnkj-train.ghlearning.com/*
// @match https://dws4jd-video.baijiayun.com/*
// @grant none
// @run-at document-start
// ==/UserScript==
(() => {
'use strict';
/* ---------------- 配置 ---------------- */
const SCAN_INTERVAL = 30_000; // 30秒
const MAX_DOM_RETRY = 5;
const IMG_CONFIDENCE = 0.85;
const DEBUG = true;
/* ------------------------------------- */
const log = (level, msg, extra = '') => {
if (!DEBUG && level === 'debug') return;
const ts = new Date().toLocaleTimeString('zh-CN');
console.log(`[AutoClose] ${ts} | ${level.padEnd(5)} |`, msg, extra);
};
/* ============ 原生鼠标点击 ============ */
async function nativeClick(x, y) {
window.scrollTo({left: 0, top: 0, behavior: 'instant'});
const send = type => {
const evt = new PointerEvent(type, {
bubbles: true, cancelable: true,
clientX: x, clientY: y, button: 'left', isPrimary: true,
pointerId: 1, pointerType: 'mouse'
});
const target = document.elementFromPoint(x, y) || document.body;
target.dispatchEvent(evt);
};
send('pointerdown');
send('pointerup');
send('click');
await new Promise(r => setTimeout(r, 120));
}
async function fireClick(el) {
el.scrollIntoView({block: 'center', behavior: 'instant'});
const rect = el.getBoundingClientRect();
const x = Math.round(rect.left + rect.width / 2);
const y = Math.round(rect.top + rect.height / 2);
await nativeClick(x, y);
}
/* ============ DOM 检测 ============ */
function tryDomClick() {
const ok15 = document.querySelector('.ant-modal-confirm-btns .ant-btn-primary>span');
if (ok15 && ok15.textContent.trim() === '确 定') {
log('info', '发现 15-min 弹窗(DOM)');
return hitButton(ok15, 'DOM-15min');
}
const nextBtn = Array.from(document.querySelectorAll('.ant-modal-confirm-btns button'))
.find(b => /继续|下一章/.test(b.textContent));
if (nextBtn) {
log('info', '发现章节结束弹窗(DOM)');
return hitButton(nextBtn, 'DOM-chapter');
}
return { ok: false, reason: '未检测到任何弹窗' };
}
async function hitButton(el, tag) {
try {
await fireClick(el);
const gone = !document.contains(el) || el.offsetParent === null;
if (gone) {
log('success', `${tag} 点击成功,弹窗已关闭`);
return { ok: true, tag };
}
log('warn', `${tag} 点击后弹窗仍存在,可能未生效`);
return { ok: false, reason: '点击无效' };
} catch (e) {
log('error', `${tag} 点击异常`, e);
return { ok: false, reason: e.message };
}
}
/* ============ 图片识别兜底 ============ */
let cvReady = false, templBtn = null;
async function initCV() {
if (cvReady) return;
return new Promise(res => {
const s = document.createElement('script');
s.src = 'https://docs.opencv.org/4.8.0/opencv.js';
s.onload = () => {
cv['onRuntimeInitialized'] = () => {
cvReady = true;
cropTemplate().then(() => res());
};
};
document.head.appendChild(s);
});
}
async function cropTemplate() {
if (typeof html2canvas === 'undefined') {
await import('https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js');
}
const btn = document.querySelector('.ant-modal-confirm-btns .ant-btn-primary');
if (!btn) return;
const canvas = await html2canvas(btn, { scale: 1 });
templBtn = cv.imread(canvas);
}
async function tryImageClick() {
if (!cvReady || !templBtn) return { ok: false, reason: 'OpenCV 或模板未就绪' };
const canvas = await html2canvas(document.body, { scale: 0.5 });
const src = cv.imread(canvas);
const dst = new cv.Mat();
cv.matchTemplate(src, templBtn, dst, cv.TM_CCOEFF_NORMED);
const result = cv.minMaxLoc(dst);
src.delete(); dst.delete();
const { maxLoc, maxVal } = result;
if (maxVal >= IMG_CONFIDENCE) {
const x = maxLoc.x + templBtn.cols / 2;
const y = maxLoc.y + templBtn.rows / 2;
log('info', `图片识别成功 置信度=${maxVal.toFixed(2)} 坐标(${x},${y})`);
await nativeClick(x, y);
return { ok: true, tag: 'IMG' };
}
return { ok: false, reason: `匹配置信度不足 (${maxVal.toFixed(2)})` };
}
/* ============ 主循环(带倒计时) ============ */
async function loop() {
let res = tryDomClick();
let retry = 0;
while (!res.ok && retry < MAX_DOM_RETRY) {
await new Promise(r => setTimeout(r, 300));
res = tryDomClick();
retry++;
}
if (!res.ok) {
log('warn', 'DOM 方式失败,尝试图片识别兜底', res.reason);
await initCV();
res = await tryImageClick();
}
if (!res.ok) {
log('info', '本轮未处理任何弹窗', res.reason);
}
/* ---- 倒计时提示 ---- */
let left = SCAN_INTERVAL / 1000;
const tick = () => {
if (left <= 0) return;
console.log(`[AutoClose] 距离下次检测还剩 ${left--} 秒`);
};
tick();
const id = setInterval(tick, 1000);
setTimeout(() => clearInterval(id), SCAN_INTERVAL);
setTimeout(loop, SCAN_INTERVAL);
}
/* ============ 启动 ============ */
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => loop());
} else {
loop();
}
})();