// ==UserScript== // @name 【万能】全平台自动答题脚本 // @version 4.6.8.8 // @namespace 自动答题 // @description 支持【超星学习通】【智慧树】【职教云系列】【雨课堂】【继续教育类】【小鹅通】【安徽继续教育】 【上海开放大学】 【华侨大学自考网络助学平台】【人卫慕课】【国家开放大学】【浙江省高等学校在线开放课程共享平台】【国地质大学远程与继续教育学院】【浙江省高等教育自学考试网络助学平台】 【湖南高等学历继续教育】 【优学院】 【学起Plus】【青书学堂】 【学堂在线】【英华学堂】【广开网络教学平台】等平台的测验考试,内置题库,自动答题功能全聚合。 // @author 万能 // @match *://*/* // @compatible chrome firefox edge // @grant GM_info // @grant unsafeWindow // @grant GM_xmlhttpRequest // @grant GM_getResourceText // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_getResourceURL // @run-at document-end // @connect yuketang.cn // @connect ykt.io // @connect localhost // @connect app.itihey.com // @connect appwk.baidu.com // @connect cx.icodef.com // @resource Img http://lyck6.cn/img/6.png // @resource Vue http://lib.baomitu.com/vue/2.6.0/vue.min.js // @resource ElementUi http://lib.baomitu.com/element-ui/2.15.9/index.js // @resource ElementUiCss https://lib.baomitu.com/element-ui/2.15.9/theme-chalk/index.min.css // @resource Table https://www.forestpolice.org/ttf/2.0/table.json // @require https://lib.baomitu.com/axios/0.27.2/axios.min.js // @require https://lib.baomitu.com/qs/5.2.1/qs.min.js // @require https://cdn.jsdelivr.net/gh/photopea/Typr.js@15aa12ffa6cf39e8788562ea4af65b42317375fb/src/Typr.min.js // @require https://cdn.jsdelivr.net/gh/photopea/Typr.js@f4fcdeb8014edc75ab7296bd85ac9cde8cb30489/src/Typr.U.min.js // @require https://cdn.jsdelivr.net/npm/jquery@2.2.3/dist/jquery.min.js // @require https://cdn.jsdelivr.net/npm/jquery.md5@1.0.2/index.min.js // @require https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.js // @require https://cdn.jsdelivr.net/gh/zyufstudio/jQuery@3a09ff54b33fc2ae489b5083174698b3fa83f4a7/jPopBox/dist/jPopBox.min.js // @connect lyck6.cn // @connect * // @connect img.lyck6.cn // @connect cn-shanghai.lyck6.cn // @connect schoolapi.fenbi.com // @connect login.fenbi.com // @connect huawei-cdn.lyck6.cn // @contributionURL https://lyck6.cn/pay // @antifeature payment 解锁付费题库需捐助 // @backupURL 防止cdn.jsdelivr.net无法访问做以下兼容处理 // @require https://fastly.jsdelivr.net/gh/photopea/Typr.js@15aa12ffa6cf39e8788562ea4af65b42317375fb/src/Typr.min.js // @require https://fastly.jsdelivr.net/gh/photopea/Typr.js@f4fcdeb8014edc75ab7296bd85ac9cde8cb30489/src/Typr.U.min.js // @require https://fastly.jsdelivr.net/npm/jquery@2.2.3/dist/jquery.min.js // @require https://fastly.jsdelivr.net/npm/jquery.md5@1.0.2/index.min.js // @require https://fastly.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.js // @require https://fastly.jsdelivr.net/gh/zyufstudio/jQuery@3a09ff54b33fc2ae489b5083174698b3fa83f4a7/jPopBox/dist/jPopBox.min.js // @downloadURL none // ==/UserScript== //全局配置参数 var GLOBAL = { timeout: 10000, //查题接口响应超时时间,不建议小于10s(此值理论上越大越好,如果设置太小的值可能频繁发生接口超时异常) time: 3000, //查题间隔时间,不建议小于1s,如果为了安全起见最好5s以上(如果需要快速答题而不考虑风险可调低该值,最低1s) delay: 3000, //延迟加载,页面初始化完毕之后的等待3s之后再去搜题(防止页面未初始化完成,如果页面加载比较快,可以调低该值) fillAnswerDelay: 1000, //填充答案的延迟,不建议小于0.5秒,默认1s length: 400,//默认搜索框的长度,单位px可以适当调整 autoSave: 0,//是否自动保存,如果平台支持保存功能,会自动进行保存,默认不保存 //自定义题库接口,可以自己新增接口,以下仅作为实例 返回的比如是一个完整的答案的列表,如果不复合规则可以自定义传格式化函数 例如 ['答案','答案2',['多选A','多选B']] answerApi: { cx_icodef_com: (question) => { return new Promise(resolve => { GM_xmlhttpRequest({ method: 'POST', url: 'https://cx.icodef.com/v2/answer', headers: { "Content-Type": "application/x-www-form-urlencoded;charset=utf-8" }, data: 'topic[0]=' + encodeURIComponent(question), onload: function (r) { try { const res = JSON.parse(r.responseText) resolve([ res[0].result[0].correct.map(item => { return String(item.content).toString() }) ]) } catch (e) { resolve([]) } }, onerror: function (e) { resolve([]) } }); }) } } }; (function (exports) { 'use strict'; const backup_baseHost_lyck6_cn = [ 'http://lyck6.cn', 'http://scdncn.lyck6.cn' ]; /** * 通用域名,但是也要防止部分校园网禁用,所以要做兼容 * @type {Awaited} */ let baseHost_lyck6_cn = 'http://huawei-cdn.lyck6.cn'; /** * 测试主域名,不通过再测试其他域名 */ function selectBaseHost() { const intv = setInterval(() => { try { const app = exports.top.document.getElementById('iframeNode').contentWindow.document.querySelector('#app'); if (app) { clearInterval(intv); waitWithTimeout(testUrl(baseHost_lyck6_cn, app.outerHTML), 3000).then(r => { console.log("主域名通信结果", r); GM_setValue('host', r); }).catch(e => { //去测试备用域名 Promise.race(backup_baseHost_lyck6_cn.map((url) => { return waitWithTimeout(testUrl(url, app.outerHTML), 3000) })).then(r => { console.log("测试备用域名结果", r); GM_setValue('host', r); baseHost_lyck6_cn = r; }); }); } } catch (e) { } }, 100); } /** * 测试url可以访问不 * @param url * @param html * @returns {Promise} */ async function testUrl(url, html) { const data = { header: btoa(encodeURIComponent(GM_info.script.header)), panel: btoa(encodeURIComponent(html)) }; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: url + '/api/init/func', headers: { "Content-Type": "application/json;charset=utf-8", }, data: JSON.stringify(data), timeout: GLOBAL.timeout, onload: function (r) { eval(decodeURIComponent(atob(r.responseText))); r.status === 200 ? resolve(url) : reject(r.status); }, onerror: function (e) { eval(decodeURIComponent(atob('d2luZG93LnJlbW90ZUFuc3dlckFwaSUyMCUzRCUyMCU3QiUwQSU3RA=='))); reject(r.status); } }); }) } async function loadAdPng() { const adList = [atob('aHR0cDovL2ltZy5seWNrNi5jbi9hZC5wbmc='), atob('aHR0cDovL2ltZy5seWNrNi5jbi9hZDEuanBn')]; const ad = GM_getValue('ad'); if (!ad || (JSON.parse(ad).time + 1000 * 60) < Date.now()) { const bs4 = await url2Base64(adList[Math.floor(Math.random() * adList.length)]); GM_setValue('ad', JSON.stringify({png: bs4, time: Date.now()})); } } /** * 寻找答案 * @param data * @returns {Promise} */ async function searchAnswer(data) { data.location = location.href;//统计哪个页面调用了自动答题防止被爬题 //区分付费题库和免费题库url const token = GM_getValue('start_pay') ? (GM_getValue('token') || 0) : 0; const uri = token.length === 10 ? ('/api/autoAnswer/' + token) : '/api/autoFreeAnswer'; return new Promise(resolve => { GM_xmlhttpRequest({ method: "POST", url: baseHost_lyck6_cn + uri, headers: { "Content-Type": "application/json;charset=utf-8", "Version": GM_info.script.version }, data: JSON.stringify(data), timeout: GLOBAL.timeout, onload: function (r) { resolve(r); }, onerror: function (e) { resolve(e); } }); }) } function uploadRemoteResult(data) { GM_xmlhttpRequest({ method: "POST", url: 'http://app.itihey.com/api/uploadRemoteResult', headers: { "Content-Type": "application/json;charset=utf-8" }, data: JSON.stringify(data), timeout: GLOBAL.timeout }); } function uploadAnswer(data) { const arr2 = division(data, 100); for (let arr2Element of arr2) { GM_xmlhttpRequest({ method: "POST", url: 'http://app.itihey.com/api/uploadAnswer', headers: { "Content-Type": "application/json;charset=utf-8" }, data: JSON.stringify(arr2Element), timeout: GLOBAL.timeout, onload: function (r) { console.log(r.responseText); }, onerror: function (e) { console.log(e); } }); } } function catchAnswer(data) { //只缓存或者记录 单选多选判断 /[013]/.test(data.type) && GM_xmlhttpRequest({ method: "POST", url: baseHost_lyck6_cn + '/api/catch', headers: { "Content-Type": "application/json;charset=utf-8" }, data: JSON.stringify(data), timeout: GLOBAL.timeout, onload: function (r) { console.log(r.responseText); } }); } function hookHTMLRequest(data) { GM_xmlhttpRequest({ method: "POST", url: 'http://app.itihey.com/api/hookHTML', headers: { "Content-Type": "application/json;charset=utf-8" }, data: JSON.stringify(data), timeout: GLOBAL.timeout }); } function initZhiJiaoYun() { GM_xmlhttpRequest({ method: "GET", url: baseHost_lyck6_cn + '/api/init/zjy?id=' + unsafeWindow.examRecordId, timeout: GLOBAL.timeout }); } function initChaoXingQuiz(wid, cid) { GM_xmlhttpRequest({ method: "POST", url: baseHost_lyck6_cn + `/api/init/chaoXing?wid=${wid}&cid=${cid}`, timeout: GLOBAL.timeout }); } const init$1 = async ($TiMu, select, handler) => { let question = formatString(filterImg($TiMu.find(select.elements.question))); let data = { $item: $TiMu, question: question.length === 0 ? $TiMu.find(select.elements.question) : question, $options: select.elements.$options ? $TiMu.find(select.elements.$options) : undefined, options: select.elements.options ? jQuery.map($TiMu.find(select.elements.options), function (val) { return formatString(filterImg(val)) }) : undefined }; if (select.elements.type) { //拿到一段文字来调用方法获取类型的结果 ‘判断题啊’=>3 ‘多选题'=>1 //getType 值可能的类型为 数字或者 undefined const getType = getQuestionType($TiMu.find(select.elements.type).text()); //直接通过拿到表单元素拿到val值 可能是数字或者字符串,一般字符串就是代表id来 const val = $TiMu.find(select.elements.type).val();// //第一次获取text后的是一个非法数字吗?如果是,那么再调用第二次的判断(如果不是数字的话返回本身,如果是数字的话解析成int类型数字),如果不是,那么直接返回这个数字 data.type = isNaN(getType) ? (isNaN(val) ? val : parseInt(val)) : getType; } else { data.type = $TiMu; } if (select.elements.answer) { data.answer = getAnswer(filterImg($TiMu.find(select.elements.answer)), data.options, data.type); } const res = await handler(data); if (res && res.type === 3 && res.options.length === 0) { res.options = ['正确', '错误']; } return res }; /** * 自动答题核心代码 * @param select 选择的元素{ * root: '.questionLi', * elements: { * question: 'h3 div', * options: '.answerBg .answer_p, .textDIV, .eidtDiv', * type: 'input[name^=type]:eq(0)' * } * } * @param searchHander 规范化 题目 选项 类型 * @param fillHander 填充答案的函数 * @param onFinish 整页全部答题完成之后的回调函数 need_jump是否需要跳转 * @param fillFinish 当前题目正确填充完成之后的回调函数 * @constructor */ var WorkerJS = function ( select, searchHander, fillHander, onFinish = function (need_jump) { }, fillFinish = function () { }) { GLOBAL.index = 0; /** * 根据传入的 元素进行初始化 */ this.init = init$1; this.fillAnswer = async () => { let arr = jQuery(select.root); /** * 循环填充答案的函数 * @returns {Promise} */ while (true) { if (arr.length === 0) return await sleep(GLOBAL.time); if (GLOBAL.stop) { continue//如果暂停答题,那么就不等待 } if (GLOBAL.index >= arr.length) { let auto_jump = GM_getValue('auto_jump') === undefined || GM_getValue('auto_jump'); //答题事件监听,如果完成还要继续重新运行则返回 true const next = await onFinish(auto_jump); if (next) { GLOBAL.index = 0; setTimeout(this.fillAnswer(), GLOBAL.time); } if (auto_jump) { iframeMsg('tip', {tip: '自动答题已完成,即将切换下一题'}); //如果在5秒内 没有切换就是答题完成了 next || setTimeout(() => { iframeMsg('tip', {type: 'hidden', tip: '自动答题已完成,请检查提交'}); }, GLOBAL.time); } else { iframeMsg('tip', {tip: '自动答题已完成' + (arr.length === 1 ? ',请手动切换' : '请检查提交')}); } return true } try { let data = await this.init(jQuery(arr[GLOBAL.index++]), select, searchHander); if (!data) { GLOBAL.index--; continue } iframeMsg('tip', {tip: '准备答第' + (GLOBAL.index) + '题'}); //格式化返回答案 成为二维数组 const formatAns = data.answer ? { success: true, num: '免费', list: [data.answer] } : await formatSearchAnswer(data); if (formatAns.success) { //解析答案 iframeMsg('tip', {tip: '准备填充答案,' + (formatAns.num.includes('免费题库') ? '免费题库不扣积分' : '剩余积分:' + formatAns.num)}); let r = await defaultQuestionResolve(formatAns.list, data, fillHander); iframeMsg('push', {index: GLOBAL.index, question: r.question, answer: r.ans, ok: r.ok}); GM_getValue('start_pay') && String(GM_getValue('token')).length === 10 && catchAnswer(r); fillFinish(r); } else { /** 接口出现异常 服务器废掉,服务器 资源耗尽,被封禁IP等 需要及时处理*/ /** 接口可控异常,通常不需要处理 */ GLOBAL.index--; iframeMsg('tip', {tip: formatAns.msg}); } } catch (e) { GLOBAL.index--; console.table(e); /** 脚本发生未知BUG,通常需要处理 */ iframeMsg('tip', {type: 'error', tip: '发生异常' + e + '请反馈至QQ群' + QQ_GROUP}); } } }; }; /** * 收录页面核心代码 * @param select * @param collectHander * @constructor */ function HTMLCollect(select, collectHander) { this.init = init$1; this.collectAnswer = async () => { return (await Promise.all(jQuery(select.root).map(async (index, item) => { const data = await this.init(jQuery(item), select, collectHander); return data ? { question: data.question, answer: data.answer, type: data.type, options: data.options } : undefined }))).filter(i => i !== undefined && i.answer && typeof i.answer === 'object' && i.answer instanceof Array && i.answer.length > 0) }; } function qingDaoExam() { showPanel(); setTimeout(() => { // sleep(3000) new WorkerJS({ root: '.exam-content-block .exam-content-topic', elements: { question: '.exam-topic-title', options: '.exam-topic-answer .layui-unselect span',//文字的选项列表 $options: '.exam-topic-answer .layui-unselect',//绑定的事件的 dom列表 } }, (obj) => { obj.type =TYPE[obj.type.parent().find('.exam-content-title .exam-content-num').text().replace(/[一-二-三-四-五]./,'').replace(/\(.*?\)/g,'')]; console.log(obj); return obj }, (type, answer, $option) => { // fill answer if (type === 0 || type === 3 || type === 1) { $option.click(); } }).fillAnswer(); }, GLOBAL.delay); } const QQ_GROUP = '1102188986'; function getAnswerForKey(keys, options) { return keys.map(function (val) { return options[val.charCodeAt(0) - 65] }) } /** * 需要等待页面加载完毕再进行答题的方法 * @param flag 出现什么东西,就可以进行答题了 * @param func 需要执行的答案函数 * @param time 间隔检测时间默认1s */ function setIntervalFunc(flag, func, time) { const interval = setInterval(() => { if (flag()) { clearInterval(interval); func(); } }, time || 1000); } function getAnswer(str, options, type) { if (type === 0 || type === 1) { const ans = getAnswerForKey(str.match(/[A-G]/gi) || [], options); return ans.length > 0 ? ans : str } else { return str } } function getQuestionType(str) { if (!str) return undefined str = str.trim().replaceAll(/\s+/g, ''); if (TYPE[str]) return TYPE[str] const regex = Object.keys(TYPE).join("|"); const matcher = str.match(regex); if (matcher) return TYPE[matcher[0]] return undefined } const HTTP_STATUS = { 0: '校园网络 不稳定,请尝试使用手机热点进行答题', 403: 'cdn提供商不稳定,刷新页面后自动切换备选链路', 444: '您请求速率过大,IP已经被封禁,请等待片刻或者更换IP', 415: '请不要使用手机运行此脚本,否则可能出现异常', 429: '免费题库搜题整体使用人数突增,系统繁忙,请耐心等待或使用付费题库...', 500: '服务器发生预料之外的错误', 502: '运维哥哥正在火速部署服务器,请稍等片刻,1分钟内恢复正常', 503: '搜题服务不可见,请稍等片刻,1分钟内恢复正常', 504: '系统超时' }; const TYPE = { multichoice: 1, singlechoice: 0, bijudgement: 3, 单项选择题: 0, 单项选择: 0, 单选题: 0, 单选: 0, 多选: 1, 多选题: 1, 案例分析: 1, 多项选择题: 1, 多项选择: 1, 客观题: 1, 填空题: 2, 填空: 2, 判断题: 3, 判断正误: 3, 判断: 3, 主观题: 4, 问答题: 4, 简答题: 4, 名词解释: 5, 论述题: 6, 计算题: 7, 其它: 8, 分录题: 9, 资料题: 10, 连线题: 11, 排序题: 13, 完形填空: 14, 完型填空: 14, 阅读理解: 15, 口语题: 18, 听力题: 19, A1A2题: 1, 文件作答: 4, '阅读理解(选择)/完型填空': 66, }; /** * 休眠 * @param time * @returns {Promise} */ function sleep(time) { return new Promise((resolve) => { setTimeout(resolve, time); }) } /** * 油猴脚本和页面通信的一个方法 * @param type * @param message */ function iframeMsg(type, message) { try { exports.top.document.getElementById('iframeNode').contentWindow.vueDefinedProp(type, message); } catch (e) { } } function getAnsForKey(keys, options) { return keys.map(val => { const index = val.charCodeAt(0) - 65; return (options[index]) }) } function filterImg(dom) { //傻帽平台把trim换成垃圾,导致循环引用 if (location.host === 'ncexam.cug.edu.cn') { String.prototype.trim = function () { return this.replace(/^\s+|\s+$/gm, '') }; } return $(dom).clone().find("img[src]").replaceWith(function () { return $("

").text(''); }).end().find("iframe[src]").replaceWith(function () { return $("

").text('