// ==UserScript==
// @name tame QTKJ
// @namespace Vionlentmonkey
// @version 3.1.15
// @description at the end of with it.
// @author someone
// @icon http://www.moj.gov.cn/favicon.ico
// @match http://218-94-1-181.sft.ipv6.jiangsu.gov.cn:8087/sfxzwsxy/*
// @match http://218-94-1-179.sft.ipv6.jiangsu.gov.cn:8087/sfxzwsxy/*
// @match http://218-94-1-175.sft.ipv6.jiangsu.gov.cn:8087/sfxzwsxy/*
// @match http://218.94.1.181:8087/sfxzwsxy/*
// @match http://218.94.1.179:8087/sfxzwsxy/*
// @match http://218.94.1.175:8087/sfxzwsxy/*
// @match http://218.94.1.181:5088/unzipapp/project/ware/attach/*
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// @require https://cdn.jsdelivr.net/npm/vm.shortcut
// @require https://greasyfork.org/scripts/381401-addstyle/code/addStyle.js
// @require https://greasyfork.org/scripts/395959-binary2text/code/binary2Text.js
// @require https://greasyfork.org/scripts/381403-fakenavigators/code/fakeNavigators.js
// @require https://greasyfork.org/scripts/395958-htmltoelements/code/htmlToElements.js
// @require https://greasyfork.org/scripts/395748-wsxy-getdata/code/wsxy_getData.js
// @require https://greasyfork.org/scripts/395937-wsxy-autoserverselect/code/wsxy_autoServerSelect.js
// @require https://greasyfork.org/scripts/395952-wsxy-autoloin/code/wsxy_autoLoin.js
// @require https://greasyfork.org/scripts/396002-wsxy-autosignup/code/wsxy_autoSignup.js
// @require https://greasyfork.org/scripts/395966-wsxy-autoExamineTest/code/wsxy_autoExamineTest.js
// @require https://greasyfork.org/scripts/395994-wsxy-handleplayer/code/wsxy_handlePlayer.js
// @require https://greasyfork.org/scripts/395997-wsxy-windowtotab/code/wsxy_windowToTab.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_openInTab
// @grant GM_notification
// @grant GM_xmlhttpRequest
// @run-at document-start
// ==/UserScript==
const body = document.body || document.documentElement;
const windowCSS = `
#Cfg {
height: auto;
background-color: lightblue;
}
#Cfg .reset_holder {
float: left;
position: relative;
bottom: -1em;
}
#Cfg .saveclose_buttons {
margin: 1em;
}
`;
const openCfg = () => {
// 避免在包含框架的页面打开造成多个设置界面重叠
if (window.top === window.self) {
GM_config.open();
}
};
GM_registerMenuCommand('江苏省司法行政网上学院个性化设置', openCfg);
GM_config.init({
id: 'Cfg',
title: '江苏省司法行政网上学院个性化设置',
fields: {
loginName: {
section: ['登录', '完整填写尝试自动登录'],
label: '账号',
labelPos: 'right',
type: 'text',
default: ''
},
pwd: {
label: '密码',
labelPos: 'right',
type: 'password',
default: ''
},
reload: {
label: '是否无限尝试',
labelPos: 'right',
type: 'checkbox',
default: false
},
open_unclose: {
section: ['操作', '后台批量打开新标签页'],
label: '快捷键',
labelPos: 'right',
type: 'text',
default: 'F8'
},
batch: {
label: '批量打开个数',
labelPos: 'right',
type: 'int',
default: 10
},
muted: {
label: '静音播放',
labelPos: 'right',
type: 'checkbox',
default: true
}
},
css: windowCSS,
events: {
save: () => {
GM_config.close();
// 服务器选择页面或登录页面自动刷新
if (
location.pathname.match(/\/sfxzwsxy\/?(serverSelect.jsp)?#?$/i) ||
location.pathname.includes('index.jsp')
) {
location.reload(true);
}
}
}
});
// 彩蛋:通关密语
const passPhrase = btoa(GM_config.get('pwd')).startsWith('TzUxbzE5');
// 从 QQ 等打开地址后会被加上奇葩后缀 http://218.94.1.179:8087/sfxzwsxy/#?tdsourcetag=s_pctim_aiomsg
const clearURLs = () => {
const tracks = [
// QQ
'?tdsourcetag=s_pctim_aiomsg',
// Weixin
'?from=groupmessage',
'&from=groupmessage',
'?from=singlemessage',
'&from=singlemessage',
'?from=timeline',
'&from=timeline',
'/type/WeixinReadCount'
];
for (let track of tracks) {
if (location.href.endsWith(track)) {
location.href = location.href.replace(track, '');
}
}
};
clearURLs();
// 统一域名
if (location.host.endsWith('.sft.ipv6.jiangsu.gov.cn:8087')) {
location.host = location.host.replace('.sft.ipv6.jiangsu.gov.cn', '').replace('-', '.');
}
// http://218.94.1.175:8087/sfxzwsxy/
if (location.pathname.match(/\/sfxzwsxy\/?(serverSelect.jsp)?#?$/i)) {
// 自动选择最空闲的服务器
setInterval(autoServerSelect, 1000);
}
// 去除登陆验证码校验
// 曾使用 OCR 识别法,参考了 https://www.cnblogs.com/ziyunfei/archive/2012/10/05/2710349.html 但准确度有限。
if (location.pathname.includes('index.jsp')) {
window.onload = () => {
autoLogin();
};
}
// 首页
if (location.pathname.includes('index.html')) {
// 修复页面过长无法完整显示的 Bug; 隐藏 密码修改提示 和 每日一题
const css = `
html[style="overflow: hidden;"] {
overflow: visible !important;
}
#layui-layer1, #layui-layer-shade1, #layui-layer2, #layui-layer-shade2 {
display: none !important;
}
`;
addStyle(css);
// 判断是否已完成任务。不等待页面载入完成就执行可能导致数据尚未成功获取而出错。
window.onload = async () => {
let userDataObj = await getUserData();
let total_hour = userDataObj.total_hour; //规定需达到的总学时
let required_hour = userDataObj.required_hour; //规定需达到的必修学时
let required_credit = userDataObj.required_credit; //规定需达到的总学分
let user_total_hour = userDataObj.user_total_hour || 0; //用户已获得的总学时
let user_required_hour = userDataObj.user_required_hour || 0; //用户已获得的必修学时
let user_required_credit = userDataObj.user_required_credit || 0; //用户已获得的总学分
if (
user_total_hour < total_hour ||
user_required_hour < required_hour ||
user_required_credit < required_credit
) {
console.log(`本学年任务尚未完成:`);
if (passPhrase) {
GM_openInTab(`${location.origin}/sfxzwsxy/jypxks/modules/homepage/homepage.jsp`, false);
} else {
// 长时间不动会被弹出,故 30 分钟刷新一次
setInterval(() => {
location.reload(true);
}, 1800000);
}
if (user_total_hour < total_hour) {
console.log(`总学时差:${total_hour - user_total_hour}`);
}
if (user_required_hour < required_hour) {
console.log(`必修学时差:${required_hour - user_required_hour}`);
}
if (user_required_credit < required_credit) {
console.log(`总学分差:${required_credit - user_required_credit}`);
}
} else {
console.log(`本学年任务已经完成`);
}
};
// 自动聚焦以便快捷键默认生效
let document_observer = new MutationObserver(() => {
if (document.getElementById('mainFrame')) {
document.getElementById('mainFrame').focus();
console.log('focus');
}
});
document_observer.observe(body, {
childList: true,
subtree: true
});
}
const autoSignupMaxTime = async () => {
// 数组存储所有未报名课程故的课时数据
let timesArray = [];
// 获取所有未报名课程的子节点。若直接使用所有必修课程,可能出现课程数字相同而对已报名课程二次点击。
let noneCoursesJdpoint = document.querySelectorAll(
'#requiredCourseTable .course > .jdpoint[style]'
);
for await (let n of noneCoursesJdpoint) {
// 由子节点获取父节点从而获取课程编号
let coursePk = n.parentNode.getElementsByClassName('coursePk')[0].textContent;
//let applyPk = await csPk2applyPk(coursePk);
let csInfo = await getCourseInfo(coursePk);
let csTime = csInfo.courseTime;
timesArray.push(csTime);
}
let longest = Math.max(...timesArray);
console.log(longest);
let indexMax = timesArray.indexOf(longest);
// 出现过该错误 indexMax === -Infinity 导致:
// TypeError: noneCoursesJdpoint[timesArray.indexOf(...)] is undefined
if (noneCoursesJdpoint[indexMax]) {
noneCoursesJdpoint[indexMax].parentNode.click();
} else {
location.reload(true);
}
const notice = document.getElementsByClassName('layui-layer-btn0');
if (notice.length === 1) {
document.getElementsByClassName('layui-layer-btn0')[0].click();
}
};
const autoOpenTrain = async applyPk => {
const trainURL =
location.origin +
'/sfxzwsxy/jypxks/modules/train/ware/course_ware_view.jsp?applyPk=' +
applyPk +
'&courseType=1';
const openClose = GM_openInTab(trainURL, true);
setTimeout(openClose.close, 1500000);
};
const autoOpenExam = async exams => {
for await (let exam of exams) {
const examURL = location.origin + '/sfxzwsxy/' + exam.getAttribute('onclick').split("'")[1];
// 魔法打开的考卷确认交卷后不能自动关闭,只得如此暴力处理,1分钟后强行关闭。
let autoExam = GM_openInTab(examURL, true);
setTimeout(autoExam.close, 60000);
}
};
// 首页培训课程 iframe
if (location.pathname.includes('homepage.jsp')) {
// 快捷键批量后台新标签页打开已报名课程播放页面
VM.registerShortcut(GM_config.get('open_unclose'), openTrains);
const autoLearn = async exams => {
if (window.top === window.self) {
// 30 分钟刷新一次
setInterval(() => {
location.reload(true);
}, 1800000);
// 用户得分状态
let userDataObj = await getUserData();
//规定需达到的总学时
let total_hour = userDataObj.total_hour;
//规定需达到的必修学时
let required_hour = userDataObj.required_hour;
//规定需达到的总学分
let required_credit = userDataObj.required_credit;
//用户已获得的总学时
let user_total_hour = userDataObj.user_total_hour || 0;
//用户已获得的必修学时
let user_required_hour = userDataObj.user_required_hour || 0;
//用户已获得的总学分
let user_required_credit = userDataObj.user_required_credit || 0;
// 全部未完成必修课程。为同时解决学时学分问题,只关注必修课。
const courses = document.querySelectorAll('#requiredCourseTable .course');
// 初始化预期学时/学分为已得值
let pendingCredit = user_required_credit;
let pendingTime = user_total_hour;
// 向预期学时/学分添加已报名课程数据
for await (let c of courses) {
let coursePk = c.getElementsByClassName('coursePk')[0].textContent;
let jdjs = c.getElementsByClassName('jdjs')[0].textContent; // 完成进度定性
if (jdjs === '完成进度') {
let csInfo = await getCourseInfo(coursePk);
let csCredit = csInfo.courseCredit;
let csTime = csInfo.courseTime;
pendingCredit += csCredit;
pendingTime += csTime;
}
}
console.log(`已获得:必修学时:${user_required_hour},总学分${user_required_credit}`);
// 判断是否已完成。首次在新标签页打开本页显然是未完成,但刷新后可能进入已完成状态。
if (
user_total_hour >= total_hour &&
user_required_hour >= required_hour &&
user_required_credit >= required_credit
) {
console.log(`本学年任务已经完成`);
return;
} else {
if (user_required_credit < required_credit && exams.length > 0) {
console.log('学分未满,有待考试课程');
await autoOpenExam(exams);
// 1分钟后考完关闭,2分钟后刷新
setTimeout(() => {
location.reload(true);
}, 120000);
} else if (pendingTime < total_hour) {
console.log(`已报名课程可得学时:${pendingTime},学分:${pendingCredit},将继续报名。`);
autoSignupMaxTime();
} else if (pendingTime >= total_hour && pendingCredit < required_credit) {
// 因为全部学习必修课,出现本状况可能很小,暂不处理
console.log(`已报名课程可得学时:${pendingTime},学分:${pendingCredit},有待处理。`);
} else if (pendingTime >= total_hour && pendingCredit >= required_credit) {
console.log('报名已完成预期');
if (user_required_hour < total_hour) {
console.log('学时未满,自动打开已报名课程,将定时关闭');
let i = 0;
for await (let c of courses) {
let applyPk = c.getElementsByClassName('applyPk')[0].textContent;
// 未报名课程 applyPk === '',可能不适合使用 for length++ 循环
if (applyPk) {
if (i <= GM_config.get('batch')) {
// 部分少见的新型播放器会弹出 confirm,避开为宜
skipNewVideoPlayerType(applyPk, autoOpenTrain);
i++;
} else {
// 跳过的课程一样会计数,可能不准
console.log(`已批量打开${i}个课程`);
return;
}
}
}
}
}
}
}
};
// 并发执行
window.onload = async () => {
// 第一页 #courseExam1,第二页 #courseExam2
const exams = document.querySelectorAll('div[id^="courseExam"] > a[title]');
Promise.all([handelExamList(exams), openKnowledge()]);
passPhrase ? autoLearn(exams) : autoSign3Credit();
};
}
// 培训课程查询 iframe
if (location.pathname.includes('course_query.jsp')) {
// 保证翻页生效
let document_observer = new MutationObserver(() => {
inquireList();
});
document_observer.observe(body, {
childList: true,
subtree: true
});
// 快捷键批量后台新标签页打开课程播放页面
VM.registerShortcut(GM_config.get('open_unclose'), openTrains);
}
// 培训课程查询 - 查看 - 题干 iframe
if (
// 点击 “下一页” 或 “上一页” 后 iframe 实际地址会去除 .jsp 之后的尾巴
location.pathname.includes('subject_list.jsp')
) {
// 清理“题干”链接
window.onload = () => {
viewSubject();
};
}
// 课程视频播放 跨域 iframe
if (location.href.startsWith('http://218.94.1.181:5088/unzipapp/project/ware/attach/')) {
// 旧播放器在 Windows 下要求 Flash,新播放器不兼容苹果系列。
fakeUA('Linux');
setInterval(killFlash, 1000);
let document_observer = new MutationObserver(() => {
autoBegin();
autoQue();
});
document_observer.observe(body, {
attributes: true,
subtree: true
});
}
// 在线考试 - 课程考试 iframe
if (location.pathname.includes('course_exam_list.jsp')) {
// 清理“参加考试”链接,新标签页打开考试及答案。
window.onload = () => {
const exams = document.querySelectorAll('a[href="#"][onclick^=openWindowFullScreen]');
handelExamList(exams);
};
}
// 考试
if (location.pathname.includes('course_examine_test.jsp')) {
if (passPhrase) {
window.onload = async () => {
autoExamineTest();
};
}
}