// ==UserScript== // @name 湖南大学继续教育小助手 // @namespace http://jxjyjd.hnu.edu.cn/ // @version 1.3.0 // @description 基于 ScriptCat 6925 号脚本二次开发修复。自动播放、静音、防暂停、切后台计时、播放完自动下一节。极简悬浮胶囊 UI。v1.1.0 补回:时长读取兜底、播放重试、保活卡住检测、ended 绑定、SDK 静音/重播、Visibility 补全、胶囊按钮+日志、SPA 重置。v1.2.0 新增:逐任务读取反刷开关并报告挂机是否有效、已计X/35分显示、剩余时长停滞诊断。v1.3.0 新增:后台挂机保活模式(静音 WebAudio 防浏览器后台节流 + Worker 心跳兜底,胶囊「挂机」按钮可切)。 // @author xqqbt & AI & dameng9 // @match https://jxjyjd.hnu.edu.cn/course/*/task/*/show // @match https://jxjyjd.hnu.edu.cn/my/course/* // @grant none // @run-at document-idle // @noframes // @license GPL-3.0 // ==/UserScript== /* * 说明: * 本脚本基于 ScriptCat (https://scriptcat.org/zh-CN/script-show-page/6925) 二次开发修复。 * 1. 保留极简悬浮胶囊 (Glassmorphism Capsule UI),但补回控制按钮与日志。 * 2. 核心逻辑对齐完整版 v1.3.3:时长读取兜底、播放重试、保活、事件绑定、SDK 静音/重播。 */ (function () { 'use strict'; if (window.__HNU_HELPER_V130__) return; if (window !== window.top) return; window.__HNU_HELPER_V130__ = true; const CFG = { autoPlay: true, autoNext: true, mute: true, replayToFinish: true, // 视频放完但学习时长未满:不跳节,重播继续计时 switchDelay: 3000, loopInterval: 3000, // 主循环:检测播放器 + 保活 + 结束检测 activityInterval: 15000, // 模拟用户活动,防无操作检测 bgKeepAlive: true, // 后台挂机计时:静音 WebAudio 防浏览器后台节流/冻结 + Worker 心跳(胶囊「挂机」按钮可切) }; const state = { win: null, playerObj: null, innerPlayer: null, videoEl: null, currentTaskId: null, playing: false, endedFired: false, completedFired: false, lastCheckTime: -1, retryCount: 0, remainLocal: null, remainStamp: 0, stallCount: 0, lastRemain: null, stallWarned: false, logLines: [], lastLoopAt: 0, kaNote: false, __ka_worker__: null, }; const VERSION = '1.3.0'; const $ = (s) => document.querySelector(s); const $$ = (s) => Array.from(document.querySelectorAll(s)); function log(msg) { const line = `[小助手v${VERSION}][${new Date().toLocaleTimeString()}] ${msg}`; console.log(line); state.logLines.push(line); if (state.logLines.length > 300) state.logLines.shift(); updateUI(); } /* ===================== Page Visibility 补丁(顶层 + iframe) ===================== */ /* 平台计时靠 iframe 内部真实可见性,补丁必须同时打到顶层和 task-content-iframe。 除读属性外,还要拦 onvisibilitychange 赋值——反刷代码常靠属性赋值挂监听。 */ function patchVisibility(win, doc) { if (!win || !doc || win.__AL_VIS_PATCHED__) return; win.__AL_VIS_PATCHED__ = true; ['hidden', 'webkitHidden', 'mozHidden', 'msHidden'].forEach(p => { if (p in doc) try { Object.defineProperty(doc, p, { configurable: true, get: () => false }); } catch (e) {} }); ['visibilityState', 'webkitVisibilityState', 'mozVisibilityState', 'msVisibilityState'].forEach(p => { if (p in doc) try { Object.defineProperty(doc, p, { configurable: true, get: () => 'visible' }); } catch (e) {} }); try { if (typeof win.hasFocus === 'function') win.hasFocus = () => true; } catch (e) {} ['onvisibilitychange', 'onblur', 'onfocus'].forEach(prop => { const target = (prop === 'onblur' || prop === 'onfocus') ? win : doc; if (prop in target) { try { Object.defineProperty(target, prop, { configurable: true, get: () => undefined, set: () => {} }); } catch (e) {} } }); ['visibilitychange', 'webkitvisibilitychange', 'mozvisibilitychange', 'blur'].forEach(evt => { const target = (evt === 'blur') ? win : doc; try { target.addEventListener(evt, e => e.stopImmediatePropagation(), true); } catch (e) {} }); if (!win.__AL_ACTIVITY_TIMER__) { win.__AL_ACTIVITY_TIMER__ = setInterval(() => { try { doc.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: 0, clientY: 0, movementX: 1, movementY: 0 })); } catch (e) {} }, CFG.activityInterval); } } patchVisibility(window, document); /* ===================== 后台挂机保活(bgKeepAlive) ===================== */ /* 为什么后台不计时:Chrome/Edge 会把后台标签页的定时器节流(最差 1 分钟 1 次), 闲置约 5 分钟后甚至整个冻结,页面自己的心跳上报断流,服务端就判"挂机不计时"。 对策: 1) 静音 WebAudio(增益 0.0005,人耳听不见)+ 循环静音