// ==UserScript==
// @name 山东滨州市专业技术人员继续教育专业/公需课培训平台-课程助手
// @namespace http://tampermonkey.net/zzzzzzys_山东滨州市专业技术人员继续教育专业/公需课培训平台-课程助手
// @version 1.0.2
// @copyright zzzzzzys.All Rights Reserved.
// @description 山东滨州市专业技术人员继续教育专业/公需课培训平台-课程助手(https://sdbz.*.yxlearning.com/index),可自动静音播放视频,防暂停。更全自动方法,请查看文档介绍!
// @author zzzzzzys
// @match https://sdbz.zyk.yxlearning.com/index*
// @match http://sdbz.gxk.yxlearning.com/index*
// @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
// @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==
(function() {
'use strict';
// 保存原始 console,防止被劫持
const _console = unsafeWindow.console;
_console.log('🚀 脚本开始加载 - 时间:', new Date().toLocaleTimeString());
_console.log('📍 当前URL:', location.href);
_console.log('📄 document.readyState:', document.readyState);
})();
class Runner {
constructor() {
this.runner = null
this.initAjaxHooker()
this.waitForDOMLoaded()
}
initAjaxHooker() {
// ajaxHooker.filter([
// // {type: 'xhr', url: 'www.example.com', method: 'GET', async: true},
// {url: "https://gw.dtdjzx.gov.cn/gwapi/us/api/study/progress2"},
// {url: "https://dywlxy.dtdjzx.gov.cn/gwapi/dywlxynet/api/user/configure"},
// ]);
const self=this
ajaxHooker.hook(request => {
if (request.url.includes('vedioValidQuestions/getQuestions')) {
request.response = res => {
window.QuestionInfo=JSON.parse(res.responseText).data
console.log("QuestionInfo:", window.QuestionInfo)
// res.responseText = '{"action": "0","success": true,"verificationDuration": 0}';
};
} else if (request.url.includes('p/play/config')) {
request.response = res => {
const json = JSON.parse(res.responseText)
console.log("play/config':");
console.log(json);
window.playConfig = json.data
// res.responseText += 'test';
};
} else if (request.url.includes('learning/learnVerify/checkCode')) {
request.abort=true
request.response=res =>{
res.responseText='{"code":0,"msg":null,"data":{"data":"请勿频繁请求","status":9999}}'
}
}else if(request.url.includes('learning/learnVerify')) {
request.abort=true
}
});
console.log("hooker:", ajaxHooker)
}
waitForDOMLoaded() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.run());
} else {
// DOM已经就绪,直接执行
this.run();
}
}
// 修改后的代码
async run() {
// 等待 body 元素存在
await new Promise(resolve => {
if (document.body) {
console.log('✅ body 已存在,直接执行');
return resolve();
}
// 如果 body 还不存在,等待它出现
const observer = new MutationObserver(() => {
if (document.body) {
observer.disconnect();
console.log('✅ 检测到 body 创建,继续执行');
resolve();
}
});
observer.observe(document.documentElement, {childList: true});
});
// 额外等待一小段时间确保 DOM 稳定
await new Promise(r => setTimeout(r, 100));
console.log('🚀 开始初始化面板,当前URL:', location.href);
const url = location.href;
this.runner = new Course("channel-hunau");
}s
}
class Course {
constructor(channel = "channel-my") {
this.panel = new AuthWindow({
VIPBtnText: "高级功能-已弃用,请前往软件学习"
})
this.channel = new BroadcastChannel(channel)
this.VIP = false
this.running = false
this.init()
}
init() {
this.panel.setOnVerifyCallback(async (data) => {
this.url = await Utils.validateCode(data)
if (this.url) {
this.panel.setTip(Utils.vipText)
this.VIP = true
return true
}
})
this.panel.setOnBegin(() => {
if (!this.running) {
this.running = true
console.log("运行时:", this.VIP)
this.run().then(r => {
this.running = false
})
}
})
this.panel.setOnVIP(async () => {
if (!this.url) {
await this.panel.handleVerify()
}
await this.runVIP()
})
this.loadVIPStatus()
try {
Swal.fire({
title: "提示",
text: Utils.swFireText,
icon: 'info',
timer: 5000,
confirmButtonText: '确定',
timerProgressBar: true,
willClose: () => {
// Utils.showUpgradeAlert()
this.panel.startAutomation()
}
});
} catch (e) {
console.error(e)
this.panel.startAutomation()
}
}
verifyCheck(){
const dialogSelector=".layui-layer1"
const self=this
const checker=setInterval(async () => {
const dom = document.querySelector(dialogSelector)
if (dom) {
console.log("检查到验证码窗口")
clearInterval(checker)
const inputSelector = "#captchaInput"
const btnSelector = ".layui-layer-btn0"
const max = 20
for (let i = 0; i < max; i++) {
try{
const input = dom.querySelector(inputSelector)
const button = dom.querySelector(btnSelector)
input.value=i
await sleep(100)
button.click()
await sleep(100)
}catch(err){
console.error(err)
break
}
}
self.verifyCheck()
}
},5000)
}
loadVIPStatus() {
if (Utils.loadStatus()) {
this.panel.setTip(Utils.vipText)
this.VIP = true
} else {
this.panel.setTip(Utils.baseText)
this.VIP = false
}
console.log("VIP:", this.VIP)
}
async runVIP() {
try {
Utils.showUpgradeAlert()
} catch
(error) {
console.error(error)
Swal.fire({
title: "高级功能执行失败!",
text: "若一直失败,请联系进行售后处理!",
icon: 'error',
confirmButtonText: '确定',
allowOutsideClick: false,
willClose: () => {
console.log(' 用户确认错误,脚本已停止');
}
});
}
}
async autoPlay(){
const video = document.querySelector('video')
if (video){
video.volume = 0
video.muted = true
if (this._preventPauseHandler) {
video.removeEventListener('pause', this._preventPauseHandler)
}
let pauseTimeout = null
this._preventPauseHandler = async () => {
if (video.ended) return
if (pauseTimeout) clearTimeout(pauseTimeout)
pauseTimeout = setTimeout(async () => {
console.log("检测到视频暂停,自动恢复播放...")
video.volume = 0
video.muted = true
try {
await video.play()
} catch (e) {
console.error("恢复播放失败:", e)
}
}, 500) // 500ms 防抖延迟
}
video.addEventListener('pause', this._preventPauseHandler)
video.addEventListener('ended', () => {
this.goNextCatalog()
}, { once: true })
await video.play()
}
}
/**
* 找到目录中当前播放项(含 #top_play 的 li),跳转到下一个未完成的条目
*/
goNextCatalog() {
const items = Array.from(document.querySelectorAll('ul.lis-content li.lis-inside-content'))
if (!items.length) {
console.log("未找到目录列表")
return
}
// 当前播放项:含有 #top_play 图标的 li
const currentIndex = items.findIndex(li => li.querySelector('#top_play'))
console.log("当前目录索引:", currentIndex)
if (currentIndex === -1) {
console.log("未找到当前播放项,尝试从第一个未学习开始")
}
// 从当前项的下一个开始,找第一个未完成的(按钮不是"已完成"/"待考试"之类的可跳过项)
const startIndex = currentIndex + 1
for (let i = startIndex; i < items.length; i++) {
const li = items[i]
const btn = li.querySelector('button')
const h2 = li.querySelector('h2')
if (!h2) continue
// 跳过已完成的(如有已完成按钮可在此过滤,当前逻辑:直接跳转下一个)
const btnText = btn ? btn.textContent.trim() : ''
console.log(`目录[${i}] 状态: ${btnText}`)
// 提取 onclick 中的跳转链接
const onclick = h2.getAttribute('onclick') || ''
const match = onclick.match(/window\.location\.href='([^']+)'/)
if (match) {
const nextUrl = match[1]
const nextTitle = h2.textContent.trim().replace(/\s+/g, ' ')
console.log("跳转下一节:", nextUrl)
Swal.fire({
title: "视频已播放完毕",
html: `即将跳转到下一节:
${nextTitle}`,
icon: 'success',
timer: 5000,
timerProgressBar: true,
confirmButtonText: '立即跳转',
showCancelButton: true,
cancelButtonText: '取消',
}).then((result) => {
// isConfirmed=点了确认,isDismissed+timer=倒计时结束,两种情况都跳转
if (result.isConfirmed || result.dismiss === Swal.DismissReason.timer) {
window.location.href = nextUrl
}
// dismiss === 'cancel' 时用户主动取消,不跳转
})
return
}
}
console.log("已是最后一节,无下一个目录")
this.finish()
}
async run() {
try {
// Utils.showUpgradeAlert()
// await processCatalog(document);
await this.autoPlay()
} catch (e) {
console.error(e)
Swal.fire({
title: "失败!",
text: `视频基础播放失败!`,
icon: 'error',
confirmButtonColor: "#FF4DAFFF",
confirmButtonText: "确定",
timer: 5000,
timerProgressBar: true,
willClose: () => {
// window.close()
}
})
}
}
sendMsg(msg) {
// 创建 BroadcastChannel
const channel = new BroadcastChannel(this.channel);
channel.postMessage(msg);
}
finish() {
if (!this.VIP) {
Swal.fire({
title: "请升级高级版!",
text: `脚本已停止!基础版只能连播几个视频!`,
icon: 'info',
confirmButtonColor: "#FF4DAFFF",
confirmButtonText: "确定",
timer: 0,
willClose: () => {
// window.close()
}
})
return
}
this.sendMsg('finish')
if (Swal) {
this.sendMsg('finish')
Swal.fire({
title: "学习完成!",
text: `学习完成,5s后页面自动关闭!`,
icon: 'success',
confirmButtonColor: "#FF4DAFFF",
confirmButtonText: "确定",
timer: 5000,
willClose: () => {
history.back()
setTimeout(()=>{
location.reload()
},1000)
}
})
}
}
async waitForVideoEnd(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.QuestionInfo.correctAnswer
console.log("答题弹窗:答案:",answer)
const options = Array.from(dialog.querySelectorAll('input'));
const correctOption = options.find(opt => {
return opt.value.includes(answer); // 根据实际页面特征调整
});
correctOption.click()
await sleep(100)
dialog.querySelector('.submit').click()
await sleep(500)
document.querySelector('.el-message-box button').click()
}
}catch (e) {}
try{
const dialog=document.querySelector('div.el-dialog[aria-label="温馨提示"]')
if(dialog) {
dialog.querySelector('button').click()
}
}catch (e) {}
} catch (e) {
console.error("checkInterval error:", e);
clearInterval(checkInterval);
setTimeout(() => {
// location.reload()
}, 2000);
}
}, 5000);
video.addEventListener('ended', () => {
clearInterval(checkInterval);
resolve()
}, {once: true}); // 监听视频结束事件
});
}
getStudyNode(selector, type = 'node', dom, timeout = 10000) {
return new Promise((resolve, reject) => {
if (!['node', 'nodeList'].includes(type)) {
console.error('Invalid type parameter. Expected "node" or "nodeList"');
reject('Invalid type parameter. Expected "node" or "nodeList"');
}
const cleanup = (timeoutId, intervalId) => {
clearTimeout(timeoutId);
clearInterval(intervalId);
};
const handleSuccess = (result, timeoutId, intervalId) => {
console.log(`${selector} ready!`);
cleanup(timeoutId, intervalId);
resolve(result);
};
const handleFailure = (timeoutId, intervalId) => {
cleanup(timeoutId, intervalId);
resolve(null);
};
const checkNode = () => {
try {
let nodes;
if (type === 'node') {
nodes = dom ? dom.querySelector(selector) : document.querySelector(selector);
return nodes
}
nodes = dom ? dom.querySelectorAll(selector) : document.querySelectorAll(selector);
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(`等待节点: ${selector}...`);
}
}, 1000);
const timeoutId = setTimeout(() => {
console.error(`节点获取超时: ${selector}`);
handleFailure(timeoutId, intervalId);
}, timeout);
});
}
checkFinish(dom) {
return dom.querySelector('.el-progress__text').innerText.includes("100")
}
checkFinish_post(dom) {
return dom.querySelector('.xxzt_icon3')
}
/**
*
* @param dom
* @returns {number} 0 视频 |1 文档|2 材料
*/
checkType(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 Utils {
constructor() {
}
// =========================================================
// 【内部标识】GM存储用的 key,一般不需要改
// =========================================================
static flag = 'hnedu123_VIP'
static js_Flag = 'hnedu123_jsCode'
static vipSign = 'hnedu123_vipSign'
// =========================================================
// 【必改配置】每个网站脚本只需修改这一块
// =========================================================
// 网站唯一ID,用于跳转到对应文档页面
static webId = '69cc7bd599c62402d8f2feef'
// 脚本启动时弹窗提示文字(简短说明脚本怎么用)
static swFireText = "请在视频播放页面使用脚本,脚本检测到视频会自动开始,脚本功能有限,也可能页面有所更新,导致脚本不能正常使用,建议下载软件使用!全自动学习所有未完成视频"
// 控制面板顶部提示文字(基础版状态)
static baseText = '建议下载软件使用!全自动学习所有未完成视频'
// 控制面板顶部提示文字(VIP/已验证状态)
static vipText = '全自动建议下载客户端使用!山东滨州市专业技术人员继续教育专业/公需课培训平台-课程助手'
// 高级功能按钮文字
static vipBtnText = "前往软件使用课程助手,全自动学习所有未完成视频!"
// 【当前脚本】功能列表,显示在功能说明弹窗左侧
static scriptFeatures = [
"辅助当前页面视频播放",
"防暂停",
"自动静音播放",
"视频播放完成自动切换",
"仅限单页面使用",
]
// 【客户端软件】功能列表,显示在功能说明弹窗右侧
static softwareFeatures = [
"输入账号密码即可全自动",
"支持批量多账号同时学习",
]
// =========================================================
// 【下载/链接配置】一般不需要改
// =========================================================
// 阿里云盘下载链接
static aliLink = "https://www.alipan.com/s/wViqbLvgSF8"
// 直链下载地址(备用)
static directLink = 'http://112.124.58.51/static/课程助手.exe'
// 授权码购买链接
static link = [
"https://68n.cn/IJ8QB",
"https://68n.cn/RM9ob",
]
// 备用网址列表(官网打不开时使用,总有一个能用)
static web_list = [
{ name: "备用地址0", url: "https://www.zzzzzzys.com/" },
{ name: "备用地址1", url: "https://zzzzzzys.lovestoblog.com/" },
{ name: "备用地址2", url: "https://zzzzzzys.us.kg/" },
{ name: "备用地址3", url: "https://zzysdocs.dpdns.org/" },
{ name: "备用地址4", url: "https://zzzzzzys.dpdns.org/" },
{ name: "备用地址5", url: "https://zzzzzzys.kesug.com/" },
{ name: "备用地址6", url: "https://zzysdocs.great-site.net/" },
]
// 官网文档链接(会自动拼接 webId)
static docLink = `${Utils.web_list[0].url}?webId=` + Utils.webId
static loadStatus() {
return false
}
static async validateCode(data) {
try {
Utils.showUpgradeAlert()
return
const res = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
'url': "https://fc-mp-8ba0e2a3-d9c9-45a0-a902-d3bde09f5afd.next.bspapp.com/utils/validCodeCas?" + new URLSearchParams(data),
method: 'GET',
onload: function (res) {
if (res.status === 200) {
const result = JSON.parse(res.response)
console.log(result)
resolve(result)
}
reject('请求失败:' + res.response)
},
onerror: function (err) {
console.error(err)
reject('请求错误!' + err.toString())
}
})
})
if (res.code !== 200) {
GM_deleteValue(Utils.flag)
GM_deleteValue(Utils.js_Flag)
throw new Error('验证失败:' + res.data)
}
Swal.fire({
title: "高级功能已启用!",
text: "校验成功!",
icon: 'success',
confirmButtonText: '确定',
});
GM_setValue(Utils.flag, true)
return res.data
} catch (e) {
console.error(e)
Swal.fire({
title: "验证失败!",
text: e.toString(),
icon: 'error',
confirmButtonText: '确定',
});
}
}
static async getJsCode(url) {
try {
let code = GM_getValue(Utils.js_Flag)
if (!code) {
const jsUrl = url
//获取js文件,然后在这里执行,然后获得结果
const jsCode = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
'url': jsUrl,
method: 'GET',
onload: function (res) {
console.log(res)
if (res.status === 200) {
const result = (res.responseText)
// console.log(result)
resolve(result)
} else {
reject('服务器拒绝:' + res.response)
}
},
onerror: function (err) {
console.error(err)
reject('请求错误!' + err.toString())
}
})
})
code = jsCode
.replace(/\\/g, '\\\\')
.replace(/'/g, '\'')
.replace(/"/g, '\"')
GM_setValue(Utils.js_Flag, 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: `
⚠️ 网页脚本有局限性
浏览器脚本运行在沙盒中,无法跨页面、无法自动登录、无法批量管理账号。
若需要 输入账号密码后全自动完成所有视频,推荐使用本地客户端。
📄 当前脚本
* 开启后,请刷新页面。每个视频大约播放三分钟后秒过!