7900964 发表于 2025-9-12 22:19:56

一个小众刷课脚本(81联聘)

这个脚本比较小众,能够实现81联聘视频课的自动播放。

在测试过程中也是本想开发一个一键操作全程无需人工参与,但试了几次效果也不是很好,

所以改为在视频页扫描所有非100%的课程,并通过点击扫描到的第一课开始播放所有未完成的课程。

因为81联聘是页面加载课程,不加载扫描不到,所以,我添加了自动下拉页面的功能,保证能够扫描到所有的课程,同时,因为视频是挂载类,链接阿里云服务,不是正常的数据链接,有时会扫描不到VIDEO,所以改动了一下传统逻辑,保证视频能够正常播放。

由于HTML结果每各页面都不同,尝试做联系,但最终失败了,所以只做了单个页面的扫描,无法实现一键学完全部课程。脚本包含了完善的错误处理机制和模态框关闭逻辑.。主要是针对无法关闭视频模态框问题进行了处理,不关闭模态窗连续播放不算播放时长。

大神可以给个意见,下边附上原代码,复制到插件就可以使用,

当然,注意一下网址,应该是不用改动!!!这是安装地址    https://scriptcat.org/zh-CN/script-show-page/4201


// ==UserScript==
// @name         81联聘-批量自动学完助手 v1.2
// @namespace    https://github.com/yourname
// @version      1.2
// @description修复稳定性问题,增强错误处理和元素检测
// @author       You
// @match      https://learn.81lianpin.com/myCourses/details/*
// @grant      none
// ==/UserScript==

/* 让页面自身的未捕获异常不再抛到控制台(仅视觉清爽,不影响功能) */
window.addEventListener('error', e => {
    if (e.filename?.includes('index.vue')) e.preventDefault();
});

(() => {
    'use strict';
    const log = (...a) => console.log('', ...a);
    const sleep = t => new Promise(r => setTimeout(r, t));
    let isProcessing = false;
    let isCompletedAlertShown = false; // 用于控制完成提示只显示一次

    /* -------------------- 路由 -------------------- */
    const onList = () => location.pathname === '/myCourses';
    const onChapter = () => location.pathname.includes('/myCourses/details/');

    /* -------------------- 面板 -------------------- */

    function drawPanel(list) {
      const old = document.getElementById('lp-helper-panel');
      if (old) old.remove();

      const box = document.createElement('div');
      box.id = 'lp-helper-panel';
      box.innerHTML = `
      <style>
      #lp-helper-panel{position:fixed;right:20px;top:100px;width:280px;background:#fff;border:1px solid #ddd;border-radius:6px;box-shadow:0 2px 8px rgba(0,0,0,.15);z-index:9999;font-size:14px;font-family:Arial;}
      #lp-helper-panel h4{margin:0;padding:10px;background:#1890ff;color:#fff;border-radius:6px 6px 0 0;}
      #lp-helper-panel ul{margin:0;padding:10px;list-style:none;max-height:300px;overflow:auto;}
      #lp-helper-panel li{margin-bottom:6px;}
      #lp-helper-panel .btn{background:#52c41a;color:#fff;padding:4px 8px;border:none;border-radius:4px;cursor:pointer;margin-left:6px;}
      #lp-helper-panel .reshow{background:#fa8c16;}
      #lp-helper-panel .processing{opacity:0.7;pointer-events:none;}
      </style>
      <h4>未学完小节(共 ${list.length} 个)</h4>
      <ul>${list.map((it, i) => `<li>${it.title} <button class="btn" data-index="${i}">单独学</button></li>`).join('')}</ul>
      <div style="padding:10px;border-top:1px solid #eee">
      <button id="lp-rescan" class="btn reshow">🔄 重新扫描</button>
      <span id="lp-countdown" style="margin-left:10px;color:#666"></span>
      </div>`;
      document.body.appendChild(box);

      /* 单独学 */
      box.querySelectorAll('.btn:not()').forEach(b => {
            b.onclick = () => handleOne(list[+b.dataset.index]);
      });

      /* 重新扫描 */
      box.querySelector('#lp-rescan').onclick = () => {
            box.remove();
            main();
      };
    }

    /* -------------------- 单个小节 -------------------- */
    async function handleOne(item) {
      log('进入小节', item.title);

      // 滚动到元素并点击
      item.node.scrollIntoView({ behavior: 'smooth', block: 'center' });
      await sleep(800);

      try {
            item.node.click();
      } catch (e) {
            log('点击元素失败:', e);
            // 尝试其他方式触发点击
            const event = new MouseEvent('click', {
                view: window,
                bubbles: true,
                cancelable: true
            });
            item.node.dispatchEvent(event);
      }

      /* 改进的视频检测机制 */
      let video = null;
      const maxWaitTime = 15000; // 15秒超时
      const startTime = Date.now();

      while (!video && Date.now() - startTime < maxWaitTime) {
            video = document.querySelector('video');
            if (video) break;
            await sleep(500);
      }

      if (!video) {
            log('未检测到 <video>,跳过本节', 'error');
            // 尝试关闭可能弹出的任何模态框
            tryCloseModal();
            return;
      }

      await waitVideoDone(video);
      await sleep(5000); // 视频播放完毕后等待5秒

      // 重新扫描并点击第一个“单独学”按钮
      main(() => {
            const panel = document.getElementById('lp-helper-panel');
            if (panel) {
                const firstButton = panel.querySelector('.btn');
                if (firstButton) {
                  firstButton.click();
                } else if (!isCompletedAlertShown) {
                  alert('您的课程已完成');
                  isCompletedAlertShown = true; // 标记提示已显示
                }
            }
      });
    }

    /* -------------------- 等待视频结束 -------------------- */
    function waitVideoDone(video) {
      return new Promise(async (resolve) => {
            const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;

            // 确保视频可以播放
            video.muted = true;
            try {
                await video.play();
            } catch (e) {
                log('自动播放失败,尝试用户交互模拟:', e);
                // 模拟用户点击视频以启动播放
                video.dispatchEvent(new Event('click'));
                await sleep(1000);
            }

            /* 1. 前 10-20 秒正常播放 */
            const firstEnd = rand(10, 20);
            video.playbackRate = 1;

            log(`前 ${firstEnd} 秒正常播放`);

            // 使用timeupdate事件监听进度
            const progressHandler = () => {
                if (video.currentTime >= firstEnd) {
                  video.removeEventListener('timeupdate', progressHandler);

                  /* 2. 跳到最后部分加速播放 */
                  const targetTime = Math.max(video.duration - 15, 0);
                  video.currentTime = targetTime;
                  video.playbackRate = 1;

                  log(`跳转到最后部分,1 倍速播放`);
                }
            };

            video.addEventListener('timeupdate', progressHandler);

            // 添加结束事件监听
            video.addEventListener('ended', async () => {
                /* 缓冲 3 秒 */
                await sleep(3000);

                /* 尝试多种方式关闭模态框 */
                await tryCloseModal();

                /* 等待页面稳定 */
                await sleep(3000);

                resolve();
            }, { once: true });

            // 添加超时机制,防止视频卡住
            setTimeout(() => {
                log('视频处理超时,尝试继续下一步');
                resolve();
            }, 300000); // 5分钟超时
      });
    }

    /* -------------------- 尝试关闭模态框 -------------------- */
    async function tryCloseModal() {
      // 多种关闭方式尝试
      const closeMethods = [
            // XPath方式
            () => {
                const closeBtn = document.evaluate(
                  '//*[@id="content"]/img',
                  document,
                  null,
                  XPathResult.FIRST_ORDERED_NODE_TYPE,
                  null
                ).singleNodeValue;
                if (closeBtn) {
                  closeBtn.click();
                  return true;
                }
                return false;
            },
            // 类名方式
            () => {
                const closeBtn = document.querySelector('.close, .ant-modal-close, ');
                if (closeBtn) {
                  closeBtn.click();
                  return true;
                }
                return false;
            },
            // ESC键模拟
            () => {
                const escEvent = new KeyboardEvent('keydown', {
                  key: 'Escape',
                  code: 'Escape',
                  keyCode: 27,
                  which: 27,
                  bubbles: true
                });
                document.dispatchEvent(escEvent);
                return true;
            },
            // 返回上一页
            () => {
                history.back();
                return true;
            }
      ];
      for (const method of closeMethods) {
            try {
                if (method()) {
                  log('成功关闭模态框');
                  await sleep(2000);
                  return;
                }
            } catch (e) {
                log('关闭模态框方法失败:', e);
            }
            await sleep(1000);
      }

      log('所有关闭方法都失败了');
    }

    /* -------------------- 扫描未学完 -------------------- */
    function main(callback) {
      // 清除之前的定时器
      if (window.lpScanInterval) {
            clearInterval(window.lpScanInterval);
      }

      // 设置新的定时器
      window.lpScanInterval = setInterval(() => {
            // 如果正在处理中,跳过扫描
            if (isProcessing) return;

            const rows = Array.from(document.querySelectorAll('span'))
            .filter(s => {
                const text = s.textContent;
                return text.includes('已学:') && !text.includes('已学:100%');
            })
            .map(s => {
                const container = s.closest('div, div, li, ') || s.parentElement;
                const clickable = container.querySelector('a, button, , ') || container;

                // 改进标题提取
                let title = '未知小节';
                try {
                  title = container.textContent.trim()
                        .split('\n')
                        .map(line => line.trim())
                        .find(line => line.length > 0 && !line.includes('已学:')) || title;
                } catch (e) {
                  log('提取标题失败:', e);
                }

                return {
                  title: title,
                  node: clickable
                };
            });

            if (rows.length > 0) {
                clearInterval(window.lpScanInterval);
                drawPanel(rows);
                if (callback) {
                  callback();
                }
            } else if (!isCompletedAlertShown) {
                alert('您的课程已完成');
                isCompletedAlertShown = true; // 标记提示已显示
            }
      }, 2000);
    }

    /* -------------------- 入口 -------------------- */
    if (onChapter()) {
      // 等待页面完全加载
      if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', main);
      } else {
            main();
      }

      // 监听路由变化
      let lastUrl = location.href;
      const observer = new MutationObserver(() => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                if (onChapter()) {
                  // 延迟一下确保页面内容加载
                  setTimeout(main, 1000);
                }
            }
      });

      observer.observe(document, { subtree: true, childList: true });
    }

    // 添加一行代码,确保脚本在后台也能继续运行
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') {
            // 当页面被最小化或切换到后台时,保持脚本活动
            setTimeout(() => {
                if (document.visibilityState === 'hidden') {
                  main();
                }
            }, 1000);
      }
    });
})();
页: [1]
查看完整版本: 一个小众刷课脚本(81联聘)