青骄第二课堂小助手: 2024 知识竞赛 | 跳过视频 | 自动完成所有课程 | 领取每日学分 | 课程自动填充答案
// ==UserScript== // @name QingJiaoHelper // @namespace http://tampermonkey.net/ // @version 0.3.5.3 // @description 青骄第二课堂小助手: 2024 知识竞赛 | 跳过视频 | 自动完成所有课程 | 领取每日学分 | 课程自动填充答案 // @author FoliageOwO // @match *://www.2-class.com/* // @match *://2-class.com/* // @grant GM_addStyle // @grant GM_getResourceText // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @license GPL-3.0 // @supportURL https://github.com/FoliageOwO/QingJiaoHelper // @require https://fastly.jsdelivr.net/npm/[email protected]/src/toastify.min.js // @require https://update.greasyfork.org/scripts/453791/lib2class.js // @require https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js // @resource toastifycss https://fastly.jsdelivr.net/npm/toastify-js/src/toastify.css // @resource spectrecss https://fastly.jsdelivr.net/gh/FoliageOwO/QingJiaoHelper/spectre.css // ==/UserScript== const apiGetGradeLevels = { method: "GET", api: "/course/getHomepageGrade", }; const apiGetCoursesByGradeLevel = { method: "GET", api: "/course/getHomepageCourseList?grade=${grade}&pageSize=50&pageNo=1", }; const apiGetSelfCoursesByGradeLevel = { method: "GET", api: "/course/getHomepageCourseList?grade=自学&pageNo=1&pageSize=500&sort=&type=${grade}", }; const apiGetTestPaperList = { method: "GET", api: "/exam/getTestPaperList?courseId=${courseId}", }; const apiCommitExam = { method: "POST", api: "/exam/commit", }; const apiAddMedal = { method: "GET", api: "/medal/addMedal", }; const apiGetBeforeResourcesByCategoryName = { method: "POST", api: "/resource/getBeforeResourcesByCategoryName", }; const apiAddPCPlayPV = { method: "POST", api: "/resource/addPCPlayPV", }; const apiLikePC = { method: "POST", api: "/resource/likePC", }; async function requestAPI(api, params, data) { const method = api.method; const origin = "https://www.2-class.com"; let url = `${origin}/api${api.api}`; for (const key in params) { url = url.replaceAll("${" + key + "}", params[key]); } if (method === "GET") { return await axios({ method: "GET", url, }) .then((response) => { const rdata = response.data; console.debug(`[${method}] ${url}`, data, rdata); if (rdata.success === false || rdata.data === null) { const errorMessage = rdata.errorMsg; const errorCode = rdata.errorCode; console.error(`API 返回错误 [${errorCode}]:${errorMessage},请刷新页面重试!`); return null; } else { return rdata; } }) .catch((reason) => { showMessage(`请求 API 失败(${reason.code}):${reason.message}\n请将控制台中的具体报错提交!`, "red"); console.error(`请求失败(${reason.status}/${reason.code})→${reason.message}→`, reason.toJSON(), reason.response, reason.stack); }); } else { return await axios({ method: "POST", url, withCredentials: true, headers: { "Content-Type": "application/json;charset=UTF-8", }, data, }).then((response) => { const rdata = response.data; console.debug(`[${method}] ${url}`, data, rdata); if (rdata.success === false || rdata.data === null) { const errorMessage = rdata.errorMsg; const errorCode = rdata.errorCode; console.error(`API 返回错误 [${errorCode}]:${errorMessage},请刷新页面重试!`); return null; } else { return rdata; } }); } } async function getAvailableGradeLevels() { return await requestAPI(apiGetGradeLevels).then((data) => { return data ? data.data.map((it) => it.value) : null; }); } async function getCoursesByGradeLevel(gradeLevel) { return await requestAPI(apiGetCoursesByGradeLevel, { grade: gradeLevel, }).then((data) => { return data ? data.data.list : null; }); } async function getSelfCoursesByGradeLevel(gradeLevel) { return await requestAPI(apiGetSelfCoursesByGradeLevel, { grade: gradeLevel, }).then((data) => { return data ? data.data.list : null; }); } async function getTestPaperList(courseId) { return await requestAPI(apiGetTestPaperList, { courseId }).then((data) => { return data ? data.data.testPaperList : null; }); } async function getCourseAnswers(courseId) { return await getTestPaperList(courseId).then((testPaperList) => { if (!isNone(testPaperList)) { const answers = testPaperList.map((column) => column.answer); console.debug(`成功获取课程 [${courseId}] 的答案`, answers); return answers.map((it) => it.split("").join(",")); } else { console.error(`无法获取课程 [${courseId}] 答案!`); return null; } }); } async function commitExam(data) { return await requestAPI(apiCommitExam, {}, data); } async function addMedal() { return await requestAPI(apiAddMedal).then((data) => { if (isNone(data)) { return null; } else { const flag = data.flag; const num = data.medalNum; if (flag) { return num; } else { return undefined; } } }); } async function getBeforeResourcesByCategoryName(data) { return await requestAPI(apiGetBeforeResourcesByCategoryName, {}, data).then((data) => data ? data.data.list.map((it) => { return { title: it.briefTitle, resourceId: it.resourceId, }; }) : null); } async function addPCPlayPV(data) { return await requestAPI(apiAddPCPlayPV, {}, data).then((data) => { return data ? data.data.r###lt : null; }); } async function likePC(data) { return await requestAPI(apiLikePC, {}, data).then((data) => { if (isNone(data)) { return null; } else { const rdata = data.data; return !Number.isNaN(Number(rdata)) || rdata.errorCode === "ALREADY_like"; } }); } const scriptName = "QingJiaoHelper"; const scriptVersion = "v0.3.5.3"; const toastifyDuration = 3 * 1000; const toastifyGravity = "top"; const toastifyPosition = "left"; const fuzzyFindConfidenceTreshold = 0.8; const __DATA__ = () => window["__DATA__"]; const reqtoken = () => __DATA__().reqtoken; const userInfo = () => __DATA__().userInfo; const isLogined = () => JSON.stringify(userInfo()) !== "{}"; const accountGradeLevel = () => isLogined() ? userInfo().department.gradeName : "未登录"; const coursesGradeLevels = async () => await getAvailableGradeLevels(); const selfCoursesGradeLevels = async () => [ "小学", "初中", "高中", "中职", "通用", ]; ("use strict"); const isTaskCoursesEnabled = () => getGMValue("qjh_isTaskCoursesEnabled", false); const isTaskSelfCourseEnabled = () => getGMValue("qjh_isTaskSelfCourseEnabled", false); const isTaskGetCreditEnabled = () => getGMValue("qjh_isTaskGetCreditEnabled", false); const isTaskFinalExaminationEnabled = () => getGMValue("qjh_isTaskFinalExaminationEnabled", false); const isFullAutomaticEmulationEnabled = () => getGMValue("qjh_isFullAutomaticEmulationEnabled", false); const isTaskCompetitionEnabled = () => getGMValue("qjh_isTaskCompetitionEnabled", true); let autoComplete = () => featureNotAvailable("自动完成"); let autoCompleteCreditsDone = () => getGMValue("qjh_autoCompleteCreditsDone", false); const features = [ { key: "courses", title: "自动完成所有课程(不包括考试)", matcher: ["/courses", "/drugControlClassroom/courses"], task: () => taskCourses(false), enabled: isTaskCoursesEnabled, }, { key: "selfCourse", title: "自动完成所有自学课程(不包括考试)", matcher: ["/selfCourse", "/drugControlClassroom/selfCourse"], task: () => taskCourses(true), enabled: isTaskSelfCourseEnabled, }, { key: "credit", title: "自动获取每日学分(会花费一段时间,请耐心等待)", matcher: ["/admin/creditCenter"], task: taskGetCredit, enabled: isTaskGetCreditEnabled, }, { key: "singleCourse", title: "单个课程自动填充答案", matcher: /\/courses\/exams\/(\d+)/, task: taskSingleCourse, enabled: () => true, }, { key: "competition", title: "知识竞赛", matcher: ["/competition"], task: taskCompetition, enabled: isTaskCompetitionEnabled, }, { key: "finalExamination", title: "期末考试", matcher: ["/courses/exams/finalExam"], task: taskFinalExamination, enabled: isTaskFinalExaminationEnabled, }, { key: "skip", title: "跳过课程视频", matcher: /\/courses\/(\d+)/, task: taskSkip, enabled: () => true, }, ]; function triggerFeatures() { if (location.pathname === "/") { showMessage(`${scriptName}\n版本:${scriptVersion}`, "green"); } features.forEach((feature) => { let matcher = feature.matcher; let isMatched = matcher instanceof RegExp ? location.pathname.match(matcher) : matcher.indexOf(location.pathname) !== -1; if (isMatched && feature.enabled()) { showMessage(`激活功能:${feature.title}`, "green"); feature.task(); } }); } (function () { for (let script of document.getElementsByTagName("script")) { if (script.innerText.indexOf("window.__DATA__") !== -1) { eval(script.innerText); } } GM_addStyle(GM_getResourceText("toastifycss")); GM_addStyle(GM_getResourceText("spectrecss")); GM_registerMenuCommand("菜单", showMenu); prepareMenu(); let pathname = location.pathname; setInterval(() => { const newPathName = location.pathname; if (newPathName !== pathname) { console.debug(`地址改变`, pathname, newPathName); pathname = newPathName; triggerFeatures(); } }); triggerFeatures(); })(); const customGradeLevels = () => getGMValue("qjh_customGradeLevels", []); const customSelfGradeLevels = () => getGMValue("qjh_customSelfGradeLevels", []); async function prepareMenu() { const menuElement = await waitForElementLoaded("#qjh-menu"); const coursesGradeLevelsList = await coursesGradeLevels(); const selfCoursesGradeLevelsList = await selfCoursesGradeLevels(); if (coursesGradeLevels === null || selfCoursesGradeLevelsList === null) { showMessage(`课程年级列表或自学课程年级列表获取失败!`, "red"); } const titleElement = await waitForElementLoaded("#qjh-menu-title"); titleElement.append(scriptVersion); for (const { selector, gradeLevels, customGradeLevelsList, customGradeLevelsListChangeHandler, } of [ { selector: "#qjh-menu-feat-courses", gradeLevels: coursesGradeLevelsList, customGradeLevelsList: customGradeLevels, customGradeLevelsListChangeHandler: (value) => GM_setValue("qjh_customGradeLevels", value), }, { selector: "#qjh-menu-feat-self-courses", gradeLevels: selfCoursesGradeLevelsList, customGradeLevelsList: customSelfGradeLevels, customGradeLevelsListChangeHandler: (value) => GM_setValue("qjh_customSelfGradeLevels", value), }, ]) { const element = await waitForElementLoaded(selector); if (gradeLevels === null) { continue; } for (const gradeLevel of gradeLevels) { const label = document.createElement("label"); label.className = "form-checkbox form-inline"; const input = document.createElement("input"); input.type = "checkbox"; input.checked = customGradeLevelsList().indexOf(gradeLevel) !== -1; input.onchange = () => { if (input.checked) { customGradeLevelsListChangeHandler(Array.of(...customGradeLevelsList(), gradeLevel)); } else { customGradeLevelsListChangeHandler(customGradeLevelsList().filter((it) => it !== gradeLevel)); } }; const i = document.createElement("i"); i.className = "form-icon"; label.appendChild(input); label.appendChild(i); label.append(gradeLevel); element.appendChild(label); } } const closeButton = await waitForElementLoaded("#qjh-menu-close-button"); closeButton.onclick = () => { menuElement.style.display = "none"; }; const toggleInputs = nodeListToArray(document.querySelectorAll("input")).filter((element) => element.getAttribute("qjh-type") === "toggle"); for (const toggleInput of toggleInputs) { const key = toggleInput.getAttribute("qjh-key"); toggleInput.checked = GM_getValue(key); toggleInput.onchange = () => { GM_setValue(key, toggleInput.checked); }; } const featButtons = nodeListToArray(document.querySelectorAll("button")).filter((element) => element.getAttribute("qjh-feat-key") !== null); for (const featButton of featButtons) { const key = featButton.getAttribute("qjh-feat-key"); const feature = features.find((feature) => feature.key === key); featButton.onclick = () => { if (feature.enabled()) { showMessage(`手动激活功能:${feature.title}`, "green"); feature.task(); } else { showMessage(`功能 ${feature.title} 未被启用!`, "red"); } }; } } async function startCourse(courseId) { const answers = await getCourseAnswers(courseId); if (answers === null) { showMessage(`[${courseId}] 无法获取当前课程的答案!`, "red"); return false; } else { location.href = `https://www.2-class.com/courses/exams/${courseId}`; } } async function taskCourses(isSelfCourses) { if (!isLogined()) { showMessage("你还没有登录!", "red"); return; } let gradeLevels = await (isSelfCourses ? selfCoursesGradeLevels : coursesGradeLevels)(); if (gradeLevels === null) { showMessage(`获取年级名列表失败,功能已中止!`, "red"); return; } console.debug("获取总年级名列表", gradeLevels); gradeLevels = isSelfCourses ? customSelfGradeLevels() : customGradeLevels(); console.debug("已选择的年级列表", gradeLevels); for (const gradeLevel of gradeLevels) { const coursesList = isSelfCourses ? await getSelfCoursesByGradeLevel(gradeLevel) : await getCoursesByGradeLevel(gradeLevel); if (coursesList === null) { showMessage(`[${gradeLevel}] 获取当前年级的课程列表失败,已跳过当前年级!`, "red"); } const courseIds = coursesList .filter((it) => !it.isFinish && it.title !== "期末考试") .map((it) => it.courseId); if (courseIds.length === 0) { console.debug(`[${gradeLevel}] 所有${isSelfCourses ? "自学" : ""}课程都是完成状态,已跳过!`); return; } console.debug(`[${gradeLevel}] 未完成的${isSelfCourses ? "自学" : ""}课程`, courseIds); let committed = 0; for (const courseId of courseIds) { if (courseId === "finalExam") { return; } if (!isNone(courseId)) { const r###lt = await startCourse(courseId); if (r###lt) { committed++; } else { console.error(`[${courseId}] 无法提交当前课程,已跳过!`); } } else { console.error(`[${gradeLevel}] 无法找到 courseId,已跳过!`); } } showMessage(`成功完成了 ${committed} 个${isSelfCourses ? "自学" : ""}课程!`, "green"); } } async function taskSingleCourse() { if (!isLogined()) { showMessage("你还没有登录!", "red"); return; } const courseId = location.pathname.match(/(\d+)/g)[0]; const answers = await getCourseAnswers(courseId); await emulateExamination(answers, "#app > div > div.home-container > div > div > div > div:nth-child(1) > div > button", "#app > div > div.home-container > div > div > div > div:nth-child(1) > div > div.exam-content-btnbox > button", "#app > div > div.home-container > div > div > div > div:nth-child(1) > div > div.exam-content-btnbox > div > button:nth-child(2)", (answers, _) => { const firstAnswer = answers.shift().toString(); return { type: "index", answer: firstAnswer, matchedQuestion: null, }; }, `答题 [${courseId}]`, answers.length, 50); const passText = await waitForElementLoaded("#app > div > div.home-container > div > div > div > div.exam-box > div > div > p.exam-pass-title"); if (passText) { const courses = []; const courseLevels = customGradeLevels(); for (const courseLevel of courseLevels) { const r###lt = await getCoursesByGradeLevel(courseLevel); for (const course of r###lt) { courses.push(course); } } const courseIds = courses .filter((it) => !it.isFinish && it.title !== "期末考试") .map((it) => it.courseId); if (courseIds.length === 0) { showMessage("所有的课程已全部自动完成!", "green"); location.href = "https://www.2-class.com/courses/"; } else { location.href = `https://www.2-class.com/courses/exams/${courseIds[0]}`; } } } async function emulateExamination(answers, startButtonSelector, primaryNextButtonSelector, secondaryNextButtonSelector, answerHandler, examinationName, size = 100, interval = 3000, afterStart = async () => { }) { let isExaminationStarted = false; let count = 0; const next = async (nextAnswers, nextButton = null) => { const questionElement = await waitForElementLoaded(".exam-content-question"); const questionText = removeStuffs(questionElement.innerText.split("\n")[0]); if (!isExaminationStarted) { const primaryNextButton = await waitForElementLoaded(primaryNextButtonSelector); isExaminationStarted = true; await next(nextAnswers, primaryNextButton); } else { let nextSecButton = nextButton; if (count > 0) { nextSecButton = await waitForElementLoaded(secondaryNextButtonSelector); } if (!isNone(size) && count < size) { nextSecButton.onclick = async () => { setTimeout(async () => await next(nextAnswers, nextSecButton), 0); }; let { type, answer, matchedQuestion } = answerHandler(answers, questionText); if (isNone(answer)) { showMessage(`未找到此题的答案,请手动回答,或等待题库更新:${questionText}`, "red"); count++; return; } else { const selections = document.getElementsByClassName("exam-single-content-box"); console.debug("选择", answer, selections); const finalQuestion = matchedQuestion || questionText; if (!isFullAutomaticEmulationEnabled()) { showMessage(`${finalQuestion ? finalQuestion + "\n" : ""}第 ${count + 1} 题答案:${type === "index" ? toDisplayAnswer(answer) : answer}`, "green"); } if (type === "text") { for (let answerText of answer.split("||")) { answerText = removeStuffs(answerText); const selectionElements = htmlCollectionToArray(selections).filter((it) => { const match = it.innerText.match(/^([A-Z])([.。,,、.])(.*)/); const answerContent = removeStuffs(match[1 + 2]); return (!isNone(answerContent) && (answerContent === answerText || fuzzyMatch(answerContent, answerText).matched)); }); selectionElements.map((it) => it.click()); } } else { for (const answerIndex of answer .split(",") .filter((it) => it !== "") .map((it) => Number(it))) { const selectionElement = selections[answerIndex]; selectionElement.click(); } } if (isFullAutomaticEmulationEnabled()) { setTimeout(() => nextSecButton.click(), interval); } count++; } } } }; const startButton = await waitForElementLoaded(startButtonSelector); if (isFullAutomaticEmulationEnabled()) { showMessage(`自动开始 ${examinationName}!`, "blue"); startButton.click(); await afterStart(); next(answers, null); } else { startButton.onclick = async () => { showMessage(`开始 ${examinationName}!`, "blue"); await afterStart(); next(answers, null); }; } } async function taskSkip() { if (!isLogined()) { showMessage("你还没有登录!", "red"); return; } const courseId = location.pathname.match(/(\d+)/g)[0]; const video = (await waitForElementLoaded("#app > div > div.home-container > div > div > div:nth-child(2) > div > div > div > div > div > video")); const videoControlButton = await waitForElementLoaded("#app > div > div.home-container > div > div > div:nth-child(2) > div > div > div > div > div > .prism-controlbar > .prism-play-btn"); videoControlButton.onclick = () => { const endTime = video.seekable.end(0); video.currentTime = endTime; }; } async function taskGetCredit() { if (!isLogined()) { showMessage("你还没有登录!", "red"); return; } const length = 5; const num = await addMedal(); if (num !== undefined) { showMessage(`成功领取禁毒徽章 [${num}]!`, "green"); } else if (num === null) { showMessage("领取徽章失败!", "red"); } else { showMessage("无法领取徽章(可能已领取过),已跳过!", "yellow"); console.warn("无法领取徽章(可能已领取过),已跳过!"); } const categories = [ { name: "public_good", tag: "read" }, { name: "ma_yun_recommend", tag: "labour" }, // the `ma_yun_recommend` has lots of sub-categorys { name: "ma_yun_recommend", tag: "movie" }, { name: "ma_yun_recommend", tag: "music" }, { name: "ma_yun_recommend", tag: "physicalEducation" }, { name: "ma_yun_recommend", tag: "arts" }, { name: "ma_yun_recommend", tag: "natural" }, { name: "ma_yun_recommend", tag: "publicWelfareFoundation" }, { name: "school_safe", tag: "safeVolunteer" }, ]; let done = 0; let failed = 0; let liked = 0; for (const category of categories) { const data = { categoryName: category.name, pageNo: 1, pageSize: 100, reqtoken: reqtoken(), tag: category.tag, }; const resources = await getBeforeResourcesByCategoryName(data); if (resources === null) { console.error(`无法获取分类 ${category.name} 的资源,已跳过!`); continue; } console.debug(`获取分类 ${category.name} 的资源`, resources); for (const resource of resources) { if (done >= length) break; const resourceId = resource.resourceId; const resourceData = { resourceId, reqtoken: reqtoken() }; const r###lt = await addPCPlayPV(resourceData); if (r###lt) { console.debug(`成功完成资源 [${resourceId}]:${resource.title}`); done++; } else { console.error(`无法完成资源 ${resourceId},已跳过!`); failed++; } const likeR###lt = await likePC(resourceData); if (likeR###lt) { console.debug(`成功点赞资源 [${resourceId}]!`); liked++; } else { console.error(`资源点赞失败 [${resourceId}],已跳过!`); } } } let beforeDone = done; const checkSuccess = setInterval(() => { if (done !== 0) { if (done === beforeDone) { showMessage(`成功完成 ${done}/${done + failed} 个资源,点赞 ${liked} 个!`, "green"); clearInterval(checkSuccess); } else { beforeDone = done; } } }, 500); } async function taskFinalExamination() { const supportedFinal = libs.supportedFinal; const gradeLevel = accountGradeLevel(); if (supportedFinal.hasOwnProperty(gradeLevel)) { const paperName = supportedFinal[gradeLevel]; let papers = libs[paperName]; await emulateExamination(papers.map((it) => it.answer), "#app > div > div.home-container > div > div > div > div:nth-child(1) > div > button", "#app > div > div.home-container > div > div > div > div:nth-child(1) > div > div.exam-content-btnbox > button", "#app > div > div.home-container > div > div > div > div:nth-child(1) > div > div.exam-content-btnbox > div > button:nth-child(2)", (_, question) => { const [answerList, n] = accurateFind(papers, question) || fuzzyFind(papers, question) || [[], 0]; return { type: "text", answer: n > 0 ? answerList.map((it) => it.answer).join("||") : null, matchedQuestion: n > 0 ? answerList.map((it) => it.realQuestion).join("||") : null, }; }, "期末考试", 10, // 一共 10 道题 3000 // 默认题目间隔 3s ); } else { showMessage(`你的年级 [${gradeLevel}] 暂未支持期末考试!`, "red"); return; } } async function taskMultiComplete() { } async function taskCompetition() { const supportedCompetition = libs.supportedCompetition; const gradeLevel = accountGradeLevel(); let gradeGroup; const gradesPrimary = { 一年级: 1, 二年级: 2, 三年级: 3, 四年级: 4, 五年级: 5, 六年级: 6, }; if (gradeLevel in gradesPrimary) { gradeGroup = "小学组"; } else { gradeGroup = "中学组"; } if (supportedCompetition.hasOwnProperty(gradeGroup)) { showMessage(`已自动选择 [${gradeGroup}] 知识竞赛题库`, "cornflowerblue"); const paperName = supportedCompetition[gradeGroup]; const papers = libs[paperName]; if (!Array.isArray(papers)) { showMessage(`[${gradeGroup}] 暂不支持知识竞赛!`, "red"); return; } await emulateExamination(papers.map((it) => it.answer), "#app > div > div.home-container > div > div > div.competiotion-exam-box-all > div.exam-box > div > div.exam_content_bottom_btn > button", "#app > div > div.home-container > div > div > div.competiotion-exam-box-all > div.exam-box > div.competition-sub > button", "#app > div > div.home-container > div > div > div.competiotion-exam-box-all > div.exam-box > div.competition-sub > button.ant-btn.ant-btn-primary", (_, question) => { const [answerList, n] = accurateFind(papers, question) || fuzzyFind(papers, question) || [[], 0]; return { type: "text", answer: n > 0 ? answerList.map((it) => it.answer).join("||") : null, matchedQuestion: n > 0 ? answerList.map((it) => it.realQuestion).join("||") : null, }; }, "知识竞赛", 20, // 最大题目数,竞赛只有 20 道题目,如果未定义并打开了 `自动下一题并提交` 会导致循环提示最后一题 80 次 3000, // 与下一题的间隔时间,单位毫秒,默认 3 秒 async () => { const gradeGroupDialog = await waitForElementLoaded("#app > div > div.home-container > div > div > div.competiotion-exam-box-all > div.dialog-mask > div"); const options = nodeListToArray(gradeGroupDialog.querySelectorAll(".option")); const filteredOptions = options.filter((it) => it.innerHTML === gradeGroup); const r###ltOption = filteredOptions[0]; if (filteredOptions.length < 1 || isNone(r###ltOption)) { showMessage(`[${gradeGroup}] 暂不支持知识竞赛!`, "red"); return; } else { r###ltOption.click(); } }); } else { showMessage(`你的年级 [${gradeLevel}] 暂未支持知识竞赛!`, "red"); return; } } function showMessage(text, color) { Toastify({ text, duration: toastifyDuration, newWindow: true, gravity: toastifyGravity, position: toastifyPosition, stopOnFocus: true, style: { background: color }, }).showToast(); } function featureNotAvailable(name = "(未知)") { showMessage(`${name} 功能当前不可用,请尝试刷新页面。如果问题依旧请上报这个 bug!`, "red"); } function isNone(obj) { return obj == undefined || obj == null; } function getGMValue(name, defaultValue) { let value = GM_getValue(name); if (isNone(value)) { value = defaultValue; GM_setValue(name, defaultValue); } return value; } async function waitForElementLoaded(querySelector) { return new Promise((resolve, reject) => { let attempts = 0; const tryFind = () => { const element = document.querySelector(querySelector); if (element) { resolve(element); } else { attempts++; if (attempts >= 30) { console.error(`无法找到元素 [${querySelector}],已放弃!`); reject(); } else { setTimeout(tryFind, 250 * Math.pow(1.1, attempts)); } } }; tryFind(); }); } function removeStuffs(string) { return isNone(string) ? null : string .replace(/\s*/g, "") .replace(/[,。?!;:—【】(),.?!;:-\[\]\(\)]/g, ""); } function toDisplayAnswer(answer) { const alphas = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); let r###lt = ""; for (const singleAnswer of answer.split(",")) { const index = Number(singleAnswer); r###lt = r###lt + alphas[index]; } return r###lt; } function nodeListToArray(nodeList) { return Array.prototype.slice.call(nodeList); } function htmlCollectionToArray(htmlCollection) { const r###lt = []; for (const element of htmlCollection) r###lt.push(element); return r###lt; } function arrayDiff(array1, array2) { return array1.concat(array2).filter((v, _, array) => { return array.indexOf(v) === array.lastIndexOf(v); }); } function fuzzyMatch(a, b) { const aChars = a.split(""); const bChars = b.split(""); const length = aChars.length > bChars.length ? aChars.length : bChars.length; const diff = arrayDiff(aChars, bChars); const diffLength = diff.length; const unconfidence = diffLength / length; return { matched: 1 - unconfidence >= fuzzyFindConfidenceTreshold, confidence: 1 - unconfidence, }; } function accurateFind(papers, question) { const r###lts = papers.filter((it) => removeStuffs(it.question) === removeStuffs(question)); if (r###lts.length > 0) { console.debug(`精确匹配问题:${question} → ${question}`); return [ r###lts.map((it) => { return { answer: it.answer, realQuestion: it.question }; }), r###lts.length, ]; } else { return null; } } function fuzzyFind(papers, question) { const chars = question.split(""); const length = chars.length; const percentages = []; for (const paper of papers) { const { matched, confidence } = fuzzyMatch(question, paper.question); if (matched) { percentages.push({ question: paper.question, answer: paper.answer, confidence, }); } } const theMostConfidents = percentages .filter((it) => it.confidence > 0) .sort((a, b) => a.confidence - b.confidence); if (theMostConfidents.length <= 0) { console.error(`模糊匹配未找到高度匹配的结果:${question}`); return null; } console.debug(`模糊匹配问题:${question} → ${theMostConfidents .map((it) => `(${it.confidence})${it.question}`) .join("||")}`); return [ theMostConfidents.map((it) => { return { answer: it.answer, realQuestion: it.question }; }), theMostConfidents.length, ]; } async function insertValue(input, value) { input.value = value; const event = new Event("input", { bubbles: true, }); const tracker = input._valueTracker; event.simulated = true; if (tracker) { tracker.setValue(value); } input.dispatchEvent(event); } async function login(account, password) { const loginButton = await waitForElementLoaded("#app > div > div.home-container > div > div > main > div.white-bg-panel > div.login_home > div > div.padding-panel.btn-panel > div > button"); loginButton.click(); const accountInput = (await waitForElementLoaded("#account")); const passwordInput = (await waitForElementLoaded("#password")); passwordInput.type = "text"; const submitButton = await waitForElementLoaded("body > div:nth-child(14) > div > div.ant-modal-wrap > div > div.ant-modal-content > div > form > div > div > div > button"); await new Promise((resolve) => setTimeout(resolve, 500)); await insertValue(accountInput, account); await insertValue(passwordInput, password); submitButton.click(); waitForElementLoaded("#login_nc") .then(async () => { showMessage("正在进行模拟滑块验证,请稍等...", "green"); await mockVerify(); waitForElementLoaded("div > div > div > div.ant-notification-notice-description").then(() => { showMessage("检测到滑块验证登入失败,请重新刷新网页并确保开发者工具处于开启状态!", "red"); }); }) .catch(() => { console.log("无滑块验证出现,已直接登入"); }); } async function mockVerify() { const mockDistance = 394; // 滑块验证的长度 const mockInterval = 20; // 滑动间隔 const mockButtonId = "nc_1_n1z"; // 滑块验证的可交互按钮 ID const verifyButton = document.getElementById(mockButtonId); const clientRect = verifyButton.getBoundingClientRect(); const x = clientRect.x; const y = clientRect.y; const mousedown = new MouseEvent("mousedown", { bubbles: true, cancelable: true, clientX: x, clientY: y, }); verifyButton.dispatchEvent(mousedown); let dx = 0; let dy = 0; const timer = setInterval(function () { const _x = x + dx; const _y = y + dy; const mousemoveEvent = new MouseEvent("mousemove", { bubbles: true, cancelable: true, clientX: _x, clientY: _y, }); verifyButton.dispatchEvent(mousemoveEvent); if (_x - x >= mockDistance) { clearInterval(timer); const mouseupEvent = new MouseEvent("mouseup", { bubbles: true, cancelable: true, clientX: _x, clientY: _y, }); verifyButton.dispatchEvent(mouseupEvent); } else { dx += Math.ceil(Math.random() * 50); } }, mockInterval); } const container = document.createElement("div"); container.setAttribute("id", "qjh-menu"); container.innerHTML = `<style> .qjh-menu { height: max-content; box-shadow: 1px 1px 10px #909090; padding: 1em; position: fixed; z-index: 999; right: 1%; top: 3%; width: 25%; -webkit-border-radius: 10px; -moz-border-radius: 10px; border-radius: 10px; } .form-inline { display: inline-block; } </style> <div class="card container qjh-menu"> <div class="card-header"> <div class="card-title text-bold h5" id="qjh-menu-title"> QingJiaoHelper <button class="btn btn-link float-right" type="button" id="qjh-menu-close-button" > ❌ </button> </div> </div> <div class="card-body"> <div class="toast toast-warning"> ⚠注意:勾选的功能会在下一次刷新页面时<mark><b>自动激活</b></mark >,未勾选的功能只能手动启用!点击<b>一键完成</b>按钮可以在这个菜单中直接完成,而不用手动跳转到对应页面。 </div> <div class="divider text-center" data-content="考试"></div> <div class="form-group"> <label class="form-switch"> <b>期末考试</b> <input type="checkbox" qjh-type="toggle" qjh-key="qjh_isTaskFinalExaminationEnabled" /> <i class="form-icon"></i> <button class="btn btn-sm mx-2" type="button"> <a href="/courses/exams/finalExam">点击跳转</a> </button> </label> </div> <div class="form-group"> <label class="form-switch"> <b>知识竞赛</b> <input type="checkbox" qjh-type="toggle" qjh-key="qjh_isTaskCompetitionEnabled" /> <i class="form-icon"></i> <button class="btn btn-sm mx-2" type="button"> <a href="/competition">点击跳转</a> </button> </label> </div> <div class="divider text-center" data-content="课程"></div> <div> <div class="form-group" id="qjh-menu-feat-courses"> <label class="form-switch"> <b>完成所选年级的课程</b> <input type="checkbox" qjh-type="toggle" qjh-key="qjh_isTaskCoursesEnabled" /> <i class="form-icon"></i> <button class="btn btn-sm mx-2" type="button" qjh-feat-key="courses"> 一键完成👉 </button> </label> </div> <div class="form-group" id="qjh-menu-feat-self-courses"> <label class="form-switch"> <b>完成所选年级的自学课程</b> <input type="checkbox" qjh-type="toggle" qjh-key="qjh_isTaskSelfCourseEnabled" /> <i class="form-icon"></i> <button class="btn btn-sm mx-2" type="button" qjh-feat-key="selfCourse" > 一键完成👉 </button> </label> </div> <div class="divider text-center" data-content="其他"></div> <div class="form-group"></div> <label class="form-switch"> <b>获取每日学分(点赞视频和领取徽章)</b> <input type="checkbox" qjh-type="toggle" qjh-key="qjh_isTaskGetCreditEnabled" /> <i class="form-icon"></i> <button class="btn btn-sm mx-2" type="button" onclick="taskGetCredit" qjh-feat-key="credit" > 一键完成👉 </button> </label> </div> <div class="form-group"> <label class="form-switch"> <b>自动开始作答、下一题和提交</b> <input type="checkbox" qjh-type="toggle" qjh-key="qjh_isFullAutomaticEmulationEnabled" /> <i class="form-icon"></i> </label> </div> </div> <div class="divider"></div> <div class="card-footer text-gray"> 本脚本由 FoliageOwO 以 <b><a href="https://www.gnu.org/licenses/gpl-3.0.en.html">GPL-3.0</a></b> 开源许可在 GitHub 开源,脚本地址:<a href="https://github.com/FoliageOwO/QingJiaoHelper" target="_blank" >GitHub</a >、<a href="https://greasyfork.org/zh-CN/scripts/452984-qingjiaohelper" target="_blank" >GreasyFork</a >。 </div> </div> </div> `; container.style.display = "none"; document.body.appendChild(container); function showMenu() { container.style.display = "unset"; }