// ==UserScript==
// @name 湖南大学国家级专业技术人员继续教育基地-课程助手
// @namespace http://userscripts.net/hnu-jx-helper
// @version 1.0.0
// @copyright edu-helper.All Rights Reserved.
// @description 湖南大学国家级专业技术人员继续教育基地-课程助手(http://jxjyjd.hnu.edu.cn/),提供基础播放辅助,支持静音防中断。完整自动化请使用桌面客户端。
// @author edu-helper
// @match http://jxjyjd.hnu.edu.cn/*
// @require https://fastly.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.min.js
// @resource https://cdn.staticfile.org/limonte-sweetalert2/11.7.1/sweetalert2.min.css
// @require https://fastly.jsdelivr.net/npm/sweetalert2@11.12.2/dist/sweetalert2.all.min.js
// @require https://scriptcat.org/lib/637/1.4.5/ajaxHooker.js#sha256=EGhGTDeet8zLCPnx8+72H15QYRfpTX4MbhyJ4lJZmyg=
// @connect fc-mp-8ba0e2a3-d9c9-45a0-a902-d3bde09f5afd.next.bspapp.com
// @connect mp-8ba0e2a3-d9c9-45a0-a902-d3bde09f5afd.cdn.bspapp.com
// @connect api.edu-helper.net
// @connect cdn.edu-helper.net
// @grant unsafeWindow
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @grant GM_info
// @grant GM_addStyle
// @run-at document-start
// @antifeature ads 有弹窗广告,介绍完全自动化软件
// @antifeature payment 脚本基础功能使用免费,但需要使用完全自动化软件时,可能收费
// ==/UserScript==
class CoreEngine {
constructor() {
this.engine = null
this.setupNetworkInterceptor()
this.ensurePageReady()
}
setupNetworkInterceptor() {
const _self=this
ajaxHooker.hook(_request => {
if (_request.url.includes('vedioValidQuestions/getQuestions')) {
_request.response = _response => {
window.quizData=JSON.parse(_response.responseText).data
console.log("QuizData:", window.quizData)
};
} else if (_request.url.includes('p/play/config')) {
_request.response = _response => {
const _json = JSON.parse(_response.responseText)
console.log("MediaCfg:");
console.log(_json);
window.mediaConfig = _json.data
};
} else if (_request.url.includes('learning/learnVerify/checkCode')) {
_request.abort=true
_request.response=_response =>{
_response.responseText='{"code":0,"msg":null,"data":{"data":"请勿频繁请求","status":9999}}'
}
}else if(_request.url.includes('learning/learnVerify')) {
_request.abort=true
}
});
console.log("Interceptor:", ajaxHooker)
}
ensurePageReady() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.run());
} else {
this.run();
}
}
run() {
const url = location.href;
this.engine = new LessonController("hnu-edu-channel")
}
}
class LessonController {
constructor(channel = "hnu-edu-comm") {
this.ui = new ControlPanel({
_vipBtnLbl: "增强模式-已停用,请切换至客户端"
})
this.commChannel = new BroadcastChannel(channel)
this.premium = false
this.active = false
this.init()
}
init() {
this.ui.onLicenseVerify(async (_payload) => {
this.endpoint = await ToolKit.validateCode(_payload)
if (this.endpoint) {
this.ui.updateStatus(ToolKit.premiumVersionNotice)
this.premium = true
return true
}
})
this.ui.onStartRequest(() => {
if (!this.active) {
this.active = true
console.log("Runtime:", this.premium)
this.run().then(r => {
this.active = false
})
}
})
this.ui.onPremiumRequest(async () => {
if (!this.endpoint) {
await this.ui.processLicense()
}
await this.executeAdvancedMode()
})
this.loadPremiumState()
try {
Swal.fire({
title: "通知",
text: ToolKit.welcomeMessage,
icon: 'info',
timer: 5000,
confirmButtonText: '确认',
timerProgressBar: true,
willClose: () => {
this.ui.beginOperation()
}
});
} catch (_ex) {
console.error(_ex)
this.ui.beginOperation()
}
}
handleCaptcha(){
const dialogSelector=".verify-layer"
const _self=this
const _checker=setInterval(async () => {
const _dom = document.querySelector(dialogSelector)
if (_dom) {
console.log("发现验证弹窗")
clearInterval(_checker)
const _inputSelector = "#verifyInput"
const _btnSelector = ".verify-submit"
const _max = 20
for (let _idx = 0; _idx < _max; _idx++) {
try{
const _input = _dom.querySelector(_inputSelector)
const _button = _dom.querySelector(_btnSelector)
_input.value=_idx
await delay(100)
_button.click()
await delay(100)
}catch(_err){
console.error(_err)
break
}
}
_self.handleCaptcha()
}
},5000)
}
loadPremiumState() {
if (ToolKit.loadStatus()) {
this.ui.updateStatus(ToolKit.premiumVersionNotice)
this.premium = true
} else {
this.ui.updateStatus(ToolKit.freeVersionNotice)
this.premium = false
}
console.log("Premium:", this.premium)
}
async executeAdvancedMode() {
try {
ToolKit.showUpgradeAlert()
} catch
(error) {
console.error(error)
Swal.fire({
title: "增强模式执行异常!",
text: "如持续异常,请联系技术支持处理!",
icon: 'error',
confirmButtonText: '确认',
allowOutsideClick: false,
willClose: () => {
console.log(' 用户确认后任务终止');
}
});
}
}
async startMediaPlayback(){
const _video = document.querySelector('video')
if (_video){
_video.volume = 0
_video.muted = true
if (this._antiPauseListener) {
_video.removeEventListener('pause', this._antiPauseListener)
}
let _pauseTimer = null
this._antiPauseListener = async () => {
if (_video.ended) return
if (_pauseTimer) clearTimeout(_pauseTimer)
_pauseTimer = setTimeout(async () => {
console.log("发现播放中断,正在恢复...")
_video.volume = 0
_video.muted = true
try {
await _video.play()
} catch (_ex) {
console.error("恢复失败:", _ex)
}
}, 500)
}
_video.addEventListener('pause', this._antiPauseListener)
_video.addEventListener('ended', () => {
this.navigateToNextChapter()
}, { once: true })
await _video.play()
}
}
navigateToNextChapter() {
const _items = Array.from(document.querySelectorAll('ul.lis-content li.lis-inside-content'))
if (!_items.length) {
console.log("目录列表未找到")
return
}
const _currIdx = _items.findIndex(li => li.querySelector('#top_play'))
console.log("CurrentIndex:", _currIdx)
if (_currIdx === -1) {
console.log("未定位当前章节,将从首个未完成项开始")
}
const _startIdx = _currIdx + 1
for (let _idx = _startIdx; _idx < _items.length; _idx++) {
const _item = _items[_idx]
const _button = _item.querySelector('button')
const _heading = _item.querySelector('h2')
if (!_heading) continue
const _btnText = _button ? _button.textContent.trim() : ''
console.log(`章节[${_idx}] 进度: ${_btnText}`)
const _onclick = _heading.getAttribute('onclick') || ''
const _match = _onclick.match(/window\.location\.href='([^']+)'/)
if (_match) {
const _nextUrl = _match[1]
const _nextTitle = _heading.textContent.trim().replace(/\s+/g, ' ')
console.log("NextChapter:", _nextUrl)
Swal.fire({
title: "本节内容已完成",
html: `准备进入下一章节:
${_nextTitle}`,
icon: 'success',
timer: 5000,
timerProgressBar: true,
confirmButtonText: '马上跳转',
showCancelButton: true,
cancelButtonText: '关闭',
}).then((_result) => {
if (_result.isConfirmed || _result.dismiss === Swal.DismissReason.timer) {
window.location.href = _nextUrl
}
})
return
}
}
console.log("已到达最后一章,无后续内容")
this.completeSession()
}
async run() {
try {
await this.startMediaPlayback()
} catch (_ex) {
console.error(_ex)
Swal.fire({
title: "出错了!",
text: `媒体播放初始化失败!`,
icon: 'error',
confirmButtonColor: "#ec4899",
confirmButtonText: "确认",
timer: 5000,
timerProgressBar: true,
willClose: () => {
}
})
}
}
broadcastEvent(_message) {
const _channel = new BroadcastChannel(this.commChannel);
_channel.postMessage(_message);
}
completeSession() {
if (!this.premium) {
Swal.fire({
title: "请升级至增强版!",
text: `任务已终止!标准版仅支持有限连续播放!`,
icon: 'info',
confirmButtonColor: "#ec4899",
confirmButtonText: "确认",
timer: 0,
willClose: () => {
}
})
return
}
this.broadcastEvent('completed')
if (Swal) {
this.broadcastEvent('completed')
Swal.fire({
title: "全部完成!",
text: `学习完毕,5秒后自动关闭页面!`,
icon: 'success',
confirmButtonColor: "#ec4899",
confirmButtonText: "确认",
timer: 5000,
willClose: () => {
history.back()
setTimeout(()=>{
location.reload()
},1000)
}
})
}
}
async monitorPlaybackProgress(_video,_dom) {
return new Promise(resolve => {
const checkInterval = setInterval(async () => {
try {
const vid=document.querySelector('video')
if(!vid){
clearInterval(checkInterval)
resolve()
}
_video.volume = 0
_video.muted = true
if (_video && _video.paused) {
console.log("播放已暂停,正在重新启动...");
_video.volume = 0
_video.muted = true
await _video.play();
}
if (!_video.src) {
console.error("媒体源缺失,准备刷新页面");
setTimeout(() => {
location.reload()
}, 5000)
}
try {
const _dialog=document.querySelector('div.el-dialog[aria-label="随机练习"]')
if(_dialog){
const _answer=window.quizData.correctAnswer
console.log("测验窗口:正确选项:",_answer)
const _options = Array.from(_dialog.querySelectorAll('input'));
const _correctOption = _options.find(opt => {
return opt.value.includes(_answer);
});
_correctOption.click()
await delay(100)
_dialog.querySelector('.submit').click()
await delay(500)
document.querySelector('.el-message-box button').click()
}
}catch (_ex) {}
try{
const _dialog=document.querySelector('div.el-dialog[aria-label="温馨提示"]')
if(_dialog) {
_dialog.querySelector('button').click()
}
}catch (_ex) {}
} catch (_ex) {
console.error("checkInterval error:", _ex);
clearInterval(checkInterval);
setTimeout(() => {
}, 2000);
}
}, 5000);
_video.addEventListener('ended', () => {
clearInterval(checkInterval);
resolve()
}, {once: true});
});
}
findElement(_query, _kind = 'node', _document, _timeout = 10000) {
return new Promise((resolve, reject) => {
if (!['node', 'nodeList'].includes(_kind)) {
console.error('Invalid _kind parameter. Expected "node" or "nodeList"');
reject('Invalid _kind parameter. Expected "node" or "nodeList"');
}
const cleanup = (_timeoutId, _intervalId) => {
clearTimeout(_timeoutId);
clearInterval(_intervalId);
};
const handleSuccess = (_result, _timeoutId, _intervalId) => {
console.log(`${_query} ready!`);
cleanup(_timeoutId, _intervalId);
resolve(_result);
};
const handleFailure = (_timeoutId, _intervalId) => {
cleanup(_timeoutId, _intervalId);
resolve(null);
};
const checkNode = () => {
try {
let _nodes;
if (_kind === 'node') {
_nodes = _document ? _document.querySelector(_query) : document.querySelector(_query);
return _nodes
}
_nodes = _document ? _document.querySelectorAll(_query) : document.querySelectorAll(_query);
return _nodes.length > 0 ? _nodes : null;
} catch (error) {
console.error('元素查询异常:', error);
reject('元素查询异常:', error)
}
};
const _intervalId = setInterval(() => {
const _result = checkNode();
if (_result) {
handleSuccess(_result, _timeoutId, _intervalId);
} else {
console.log(`等待元素: ${_query}...`);
}
}, 1000);
const _timeoutId = setTimeout(() => {
console.error(`元素查找超时: ${_query}`);
handleFailure(_timeoutId, _intervalId);
}, _timeout);
});
}
verifyCompletion(_dom) {
return _dom.querySelector('.el-progress__text').innerText.includes("100")
}
checkPostStatus(_dom) {
return _dom.querySelector('.xxzt_icon3')
}
identifyContentType(_dom) {
if (_dom.querySelector('.font-syllabus-online-video')) {
return 0
} else if (_dom.querySelector('.font-syllabus-page')) {
return 1
} else if (_dom.querySelector('.font-syllabus-material')) {
return 2
}
}
}
class ToolKit {
constructor() {
}
static storageKey = 'hnu_edu_premium'
static scriptCacheKey = 'hnu_edu_script_cache'
static fastModeFlag = 'hnu_edu_fast_mode'
static siteIdentifier = '3ed13579a43472299a123aea'
static welcomeMessage = "请在课程播放界面启动本工具,系统将自动识别媒体资源并开始播放。受限于浏览器环境,部分功能可能因平台更新而失效,建议下载独立客户端以获得完整自动化体验。"
static freeVersionNotice = '推荐使用客户端程序!一键完成全部未学内容'
static premiumVersionNotice = '完整自动化请使用桌面应用!湖南大学继教平台-辅助工具'
static upgradeButtonLabel = "打开客户端实现全自动课程学习"
static currentCapabilities = [
"支持当前页面媒体播放",
"防止播放中断",
"自动静音连续播放",
"仅支持当前页面",
]
static appCapabilities = [
"登录凭证后全自动运行",
"一键自动学习未学课程",
"支持多账户批量并行处理",
]
static cloudStorageUrl = "https://www.alipan.com/s/wViqbLvgSF8"
static mirrorDownloadUrl = 'https://eduhelper.lanzouu.com/b00zxor42h'
static purchaseUrls = [
"https://68n.cn/IJ8QB",
"https://68n.cn/RM9ob",
]
static mirrorSites = [
{ name: "备用地址0", url: "https://www.eduhelper.net/" },
{ name: "备用地址1", url: "https://eduhelper.lovestoblog.com/" },
{ name: "备用地址2", url: "https://eduhelper.us.kg/" },
{ name: "备用地址3", url: "https://edu-docs.dpdns.org/" },
{ name: "备用地址4", url: "https://eduhelper.dpdns.org/" },
{ name: "备用地址5", url: "https://eduhelper.kesug.com/" },
{ name: "备用地址6", url: "https://edu-docs.great-site.net/" },
]
static documentationUrl = `${ToolKit.mirrorSites[0].url}?webId=` + ToolKit.siteIdentifier
static loadStatus() {
return false
}
static async validateCode(_payload) {
try {
ToolKit.showUpgradeAlert()
return
const _res = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
'url': "https://api.edu-helper.net/api/v1/external/quota/external/proxy/script-activate?" + new URLSearchParams(_payload),
method: 'GET',
onload: function (_response) {
if (_response.status === 200) {
const _result = JSON.parse(_response.response)
console.log(_result)
resolve(_result)
}
reject('服务端返回错误:' + _response.response)
},
onerror: function (_error) {
console.error(_error)
reject('网络请求异常!' + _error.toString())
}
})
})
if (_res.code !== 200) {
GM_deleteValue(ToolKit.storageKey)
GM_deleteValue(ToolKit.scriptCacheKey)
throw new Error('校验失败:' + _res.data)
}
Swal.fire({
title: "增强模式已激活!",
text: "验证通过!",
icon: 'success',
confirmButtonText: '确认',
});
GM_setValue(ToolKit.storageKey, true)
return _res.data
} catch (_ex) {
console.error(_ex)
Swal.fire({
title: "校验未通过!",
text: _ex.toString(),
icon: 'error',
confirmButtonText: '确认',
});
}
}
static async getJsCode(url) {
try {
let _code = GM_getValue(ToolKit.scriptCacheKey)
if (!_code) {
const jsUrl = url
const _jsCode = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
'url': jsUrl,
method: 'GET',
onload: function (_response) {
console.log(_response)
if (_response.status === 200) {
const _result = (_response.responseText)
resolve(_result)
} else {
reject('服务端拒绝:' + _response.response)
}
},
onerror: function (_error) {
console.error(_error)
reject('网络请求异常!' + _error.toString())
}
})
})
_code = _jsCode
.replace(/\\/g, '\\\\')
.replace(/'/g, '\'')
.replace(/"/g, '\"')
GM_setValue(ToolKit.scriptCacheKey, _code)
}
return _code
} catch (error) {
console.error('远端资源加载失败:', error);
throw new Error("远端资源加载失败")
}
}
static showLinkSwal() {
const _link = [
"https://68n.cn/IJ8QB",
"https://68n.cn/RM9ob",
]
Swal.fire({
title: ' 增强模式激活',
html: `
⚠️ 浏览器扩展存在限制
由于浏览器安全沙箱机制,不能跨标签页操作、不支持自动认证、无法批量处理多账户。
如需 登录后全自动完成全部课程,建议使用本地应用程序。
📄 本扩展
* 启用后请刷新页面。每个视频播放约三分钟后自动完成!