在线视频自动点弹窗的脚本

功能说明

每年继续教育要求视频学习,并在线考试。但是每15分钟会弹出提示框,点确定后才能及时播放视频计时,有时候一忙就忘了,这个工具可用模拟鼠标自动点确定继续播放,解放双手,提高效率。

适用于河南专技在线(会计人员)继续教育网络学习平台(网址:https://hnkj.ghlearning.com)

使用步骤:

  1. 复制上面全部代码 → 打开 Tampermonkey → 找到旧脚本 → 整页替换 → Ctrl+S 保存。
  2. 刷新学习页面,什么都不用动,继续挂机。
  3. 等 15 分钟弹窗出现,观察控制台:
  4. [AutoClose] xx:xx:xx | success | DOM-15min 点击成功,弹窗已关闭
  5. 出现即代表真鼠标点击生效,视频会继续播放。
// ==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();
  }
})();

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注