QingJiaoHelper

青骄第二课堂小助手: 2024 知识竞赛 | 跳过视频 | 自动完成所有课程 | 领取每日学分 | 课程自动填充答案

  1. // ==UserScript==
  2. // @name QingJiaoHelper
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.3.5.3
  5. // @description 青骄第二课堂小助手: 2024 知识竞赛 | 跳过视频 | 自动完成所有课程 | 领取每日学分 | 课程自动填充答案
  6. // @author FoliageOwO
  7. // @match *://www.2-class.com/*
  8. // @match *://2-class.com/*
  9. // @grant GM_addStyle
  10. // @grant GM_getResourceText
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @license GPL-3.0
  15. // @supportURL https://github.com/FoliageOwO/QingJiaoHelper
  16. // @require https://fastly.jsdelivr.net/npm/toastify-js@1.12.0/src/toastify.min.js
  17. // @require https://update.greasyfork.org/scripts/453791/lib2class.js
  18. // @require https://fastly.jsdelivr.net/npm/axios@1.3.6/dist/axios.min.js
  19. // @resource toastifycss https://fastly.jsdelivr.net/npm/toastify-js/src/toastify.css
  20. // @resource spectrecss https://fastly.jsdelivr.net/gh/FoliageOwO/QingJiaoHelper/spectre.css
  21. // ==/UserScript==
  22. const apiGetGradeLevels = {
  23. method: "GET",
  24. api: "/course/getHomepageGrade",
  25. };
  26. const apiGetCoursesByGradeLevel = {
  27. method: "GET",
  28. api: "/course/getHomepageCourseList?grade=${grade}&pageSize=50&pageNo=1",
  29. };
  30. const apiGetSelfCoursesByGradeLevel = {
  31. method: "GET",
  32. api: "/course/getHomepageCourseList?grade=自学&pageNo=1&pageSize=500&sort=&type=${grade}",
  33. };
  34. const apiGetTestPaperList = {
  35. method: "GET",
  36. api: "/exam/getTestPaperList?courseId=${courseId}",
  37. };
  38. const apiCommitExam = {
  39. method: "POST",
  40. api: "/exam/commit",
  41. };
  42. const apiAddMedal = {
  43. method: "GET",
  44. api: "/medal/addMedal",
  45. };
  46. const apiGetBeforeResourcesByCategoryName = {
  47. method: "POST",
  48. api: "/resource/getBeforeResourcesByCategoryName",
  49. };
  50. const apiAddPCPlayPV = {
  51. method: "POST",
  52. api: "/resource/addPCPlayPV",
  53. };
  54. const apiLikePC = {
  55. method: "POST",
  56. api: "/resource/likePC",
  57. };
  58. async function requestAPI(api, params, data) {
  59. const method = api.method;
  60. const origin = "https://www.2-class.com";
  61. let url = `${origin}/api${api.api}`;
  62. for (const key in params) {
  63. url = url.replaceAll("${" + key + "}", params[key]);
  64. }
  65. if (method === "GET") {
  66. return await axios({
  67. method: "GET",
  68. url,
  69. })
  70. .then((response) => {
  71. const rdata = response.data;
  72. console.debug(`[${method}] ${url}`, data, rdata);
  73. if (rdata.success === false || rdata.data === null) {
  74. const errorMessage = rdata.errorMsg;
  75. const errorCode = rdata.errorCode;
  76. console.error(`API 返回错误 [${errorCode}]:${errorMessage},请刷新页面重试!`);
  77. return null;
  78. }
  79. else {
  80. return rdata;
  81. }
  82. })
  83. .catch((reason) => {
  84. showMessage(`请求 API 失败(${reason.code}):${reason.message}\n请将控制台中的具体报错提交!`, "red");
  85. console.error(`请求失败(${reason.status}/${reason.code})→${reason.message}→`, reason.toJSON(), reason.response, reason.stack);
  86. });
  87. }
  88. else {
  89. return await axios({
  90. method: "POST",
  91. url,
  92. withCredentials: true,
  93. headers: {
  94. "Content-Type": "application/json;charset=UTF-8",
  95. },
  96. data,
  97. }).then((response) => {
  98. const rdata = response.data;
  99. console.debug(`[${method}] ${url}`, data, rdata);
  100. if (rdata.success === false || rdata.data === null) {
  101. const errorMessage = rdata.errorMsg;
  102. const errorCode = rdata.errorCode;
  103. console.error(`API 返回错误 [${errorCode}]:${errorMessage},请刷新页面重试!`);
  104. return null;
  105. }
  106. else {
  107. return rdata;
  108. }
  109. });
  110. }
  111. }
  112. async function getAvailableGradeLevels() {
  113. return await requestAPI(apiGetGradeLevels).then((data) => {
  114. return data ? data.data.map((it) => it.value) : null;
  115. });
  116. }
  117. async function getCoursesByGradeLevel(gradeLevel) {
  118. return await requestAPI(apiGetCoursesByGradeLevel, {
  119. grade: gradeLevel,
  120. }).then((data) => {
  121. return data ? data.data.list : null;
  122. });
  123. }
  124. async function getSelfCoursesByGradeLevel(gradeLevel) {
  125. return await requestAPI(apiGetSelfCoursesByGradeLevel, {
  126. grade: gradeLevel,
  127. }).then((data) => {
  128. return data ? data.data.list : null;
  129. });
  130. }
  131. async function getTestPaperList(courseId) {
  132. return await requestAPI(apiGetTestPaperList, { courseId }).then((data) => {
  133. return data ? data.data.testPaperList : null;
  134. });
  135. }
  136. async function getCourseAnswers(courseId) {
  137. return await getTestPaperList(courseId).then((testPaperList) => {
  138. if (!isNone(testPaperList)) {
  139. const answers = testPaperList.map((column) => column.answer);
  140. console.debug(`成功获取课程 [${courseId}] 的答案`, answers);
  141. return answers.map((it) => it.split("").join(","));
  142. }
  143. else {
  144. console.error(`无法获取课程 [${courseId}] 答案!`);
  145. return null;
  146. }
  147. });
  148. }
  149. async function commitExam(data) {
  150. return await requestAPI(apiCommitExam, {}, data);
  151. }
  152. async function addMedal() {
  153. return await requestAPI(apiAddMedal).then((data) => {
  154. if (isNone(data)) {
  155. return null;
  156. }
  157. else {
  158. const flag = data.flag;
  159. const num = data.medalNum;
  160. if (flag) {
  161. return num;
  162. }
  163. else {
  164. return undefined;
  165. }
  166. }
  167. });
  168. }
  169. async function getBeforeResourcesByCategoryName(data) {
  170. return await requestAPI(apiGetBeforeResourcesByCategoryName, {}, data).then((data) => data
  171. ? data.data.list.map((it) => {
  172. return {
  173. title: it.briefTitle,
  174. resourceId: it.resourceId,
  175. };
  176. })
  177. : null);
  178. }
  179. async function addPCPlayPV(data) {
  180. return await requestAPI(apiAddPCPlayPV, {}, data).then((data) => {
  181. return data ? data.data.result : null;
  182. });
  183. }
  184. async function likePC(data) {
  185. return await requestAPI(apiLikePC, {}, data).then((data) => {
  186. if (isNone(data)) {
  187. return null;
  188. }
  189. else {
  190. const rdata = data.data;
  191. return !Number.isNaN(Number(rdata)) || rdata.errorCode === "ALREADY_like";
  192. }
  193. });
  194. }
  195. const scriptName = "QingJiaoHelper";
  196. const scriptVersion = "v0.3.5.3";
  197. const toastifyDuration = 3 * 1000;
  198. const toastifyGravity = "top";
  199. const toastifyPosition = "left";
  200. const fuzzyFindConfidenceTreshold = 0.8;
  201. const __DATA__ = () => window["__DATA__"];
  202. const reqtoken = () => __DATA__().reqtoken;
  203. const userInfo = () => __DATA__().userInfo;
  204. const isLogined = () => JSON.stringify(userInfo()) !== "{}";
  205. const accountGradeLevel = () => isLogined() ? userInfo().department.gradeName : "未登录";
  206. const coursesGradeLevels = async () => await getAvailableGradeLevels();
  207. const selfCoursesGradeLevels = async () => [
  208. "小学",
  209. "初中",
  210. "高中",
  211. "中职",
  212. "通用",
  213. ];
  214. ("use strict");
  215. const isTaskCoursesEnabled = () => getGMValue("qjh_isTaskCoursesEnabled", false);
  216. const isTaskSelfCourseEnabled = () => getGMValue("qjh_isTaskSelfCourseEnabled", false);
  217. const isTaskGetCreditEnabled = () => getGMValue("qjh_isTaskGetCreditEnabled", false);
  218. const isTaskFinalExaminationEnabled = () => getGMValue("qjh_isTaskFinalExaminationEnabled", false);
  219. const isFullAutomaticEmulationEnabled = () => getGMValue("qjh_isFullAutomaticEmulationEnabled", false);
  220. const isTaskCompetitionEnabled = () => getGMValue("qjh_isTaskCompetitionEnabled", true);
  221. let autoComplete = () => featureNotAvailable("自动完成");
  222. let autoCompleteCreditsDone = () => getGMValue("qjh_autoCompleteCreditsDone", false);
  223. const features = [
  224. {
  225. key: "courses",
  226. title: "自动完成所有课程(不包括考试)",
  227. matcher: ["/courses", "/drugControlClassroom/courses"],
  228. task: () => taskCourses(false),
  229. enabled: isTaskCoursesEnabled,
  230. },
  231. {
  232. key: "selfCourse",
  233. title: "自动完成所有自学课程(不包括考试)",
  234. matcher: ["/selfCourse", "/drugControlClassroom/selfCourse"],
  235. task: () => taskCourses(true),
  236. enabled: isTaskSelfCourseEnabled,
  237. },
  238. {
  239. key: "credit",
  240. title: "自动获取每日学分(会花费一段时间,请耐心等待)",
  241. matcher: ["/admin/creditCenter"],
  242. task: taskGetCredit,
  243. enabled: isTaskGetCreditEnabled,
  244. },
  245. {
  246. key: "singleCourse",
  247. title: "单个课程自动填充答案",
  248. matcher: /\/courses\/exams\/(\d+)/,
  249. task: taskSingleCourse,
  250. enabled: () => true,
  251. },
  252. {
  253. key: "competition",
  254. title: "知识竞赛",
  255. matcher: ["/competition"],
  256. task: taskCompetition,
  257. enabled: isTaskCompetitionEnabled,
  258. },
  259. {
  260. key: "finalExamination",
  261. title: "期末考试",
  262. matcher: ["/courses/exams/finalExam"],
  263. task: taskFinalExamination,
  264. enabled: isTaskFinalExaminationEnabled,
  265. },
  266. {
  267. key: "skip",
  268. title: "跳过课程视频",
  269. matcher: /\/courses\/(\d+)/,
  270. task: taskSkip,
  271. enabled: () => true,
  272. },
  273. ];
  274. function triggerFeatures() {
  275. if (location.pathname === "/") {
  276. showMessage(`${scriptName}\n版本:${scriptVersion}`, "green");
  277. }
  278. features.forEach((feature) => {
  279. let matcher = feature.matcher;
  280. let isMatched = matcher instanceof RegExp
  281. ? location.pathname.match(matcher)
  282. : matcher.indexOf(location.pathname) !== -1;
  283. if (isMatched && feature.enabled()) {
  284. showMessage(`激活功能:${feature.title}`, "green");
  285. feature.task();
  286. }
  287. });
  288. }
  289. (function () {
  290. for (let script of document.getElementsByTagName("script")) {
  291. if (script.innerText.indexOf("window.__DATA__") !== -1) {
  292. eval(script.innerText);
  293. }
  294. }
  295. GM_addStyle(GM_getResourceText("toastifycss"));
  296. GM_addStyle(GM_getResourceText("spectrecss"));
  297. GM_registerMenuCommand("菜单", showMenu);
  298. prepareMenu();
  299. let pathname = location.pathname;
  300. setInterval(() => {
  301. const newPathName = location.pathname;
  302. if (newPathName !== pathname) {
  303. console.debug(`地址改变`, pathname, newPathName);
  304. pathname = newPathName;
  305. triggerFeatures();
  306. }
  307. });
  308. triggerFeatures();
  309. })();
  310. const customGradeLevels = () => getGMValue("qjh_customGradeLevels", []);
  311. const customSelfGradeLevels = () => getGMValue("qjh_customSelfGradeLevels", []);
  312. async function prepareMenu() {
  313. const menuElement = await waitForElementLoaded("#qjh-menu");
  314. const coursesGradeLevelsList = await coursesGradeLevels();
  315. const selfCoursesGradeLevelsList = await selfCoursesGradeLevels();
  316. if (coursesGradeLevels === null || selfCoursesGradeLevelsList === null) {
  317. showMessage(`课程年级列表或自学课程年级列表获取失败!`, "red");
  318. }
  319. const titleElement = await waitForElementLoaded("#qjh-menu-title");
  320. titleElement.append(scriptVersion);
  321. for (const { selector, gradeLevels, customGradeLevelsList, customGradeLevelsListChangeHandler, } of [
  322. {
  323. selector: "#qjh-menu-feat-courses",
  324. gradeLevels: coursesGradeLevelsList,
  325. customGradeLevelsList: customGradeLevels,
  326. customGradeLevelsListChangeHandler: (value) => GM_setValue("qjh_customGradeLevels", value),
  327. },
  328. {
  329. selector: "#qjh-menu-feat-self-courses",
  330. gradeLevels: selfCoursesGradeLevelsList,
  331. customGradeLevelsList: customSelfGradeLevels,
  332. customGradeLevelsListChangeHandler: (value) => GM_setValue("qjh_customSelfGradeLevels", value),
  333. },
  334. ]) {
  335. const element = await waitForElementLoaded(selector);
  336. if (gradeLevels === null) {
  337. continue;
  338. }
  339. for (const gradeLevel of gradeLevels) {
  340. const label = document.createElement("label");
  341. label.className = "form-checkbox form-inline";
  342. const input = document.createElement("input");
  343. input.type = "checkbox";
  344. input.checked =
  345. customGradeLevelsList().indexOf(gradeLevel) !== -1;
  346. input.onchange = () => {
  347. if (input.checked) {
  348. customGradeLevelsListChangeHandler(Array.of(...customGradeLevelsList(), gradeLevel));
  349. }
  350. else {
  351. customGradeLevelsListChangeHandler(customGradeLevelsList().filter((it) => it !== gradeLevel));
  352. }
  353. };
  354. const i = document.createElement("i");
  355. i.className = "form-icon";
  356. label.appendChild(input);
  357. label.appendChild(i);
  358. label.append(gradeLevel);
  359. element.appendChild(label);
  360. }
  361. }
  362. const closeButton = await waitForElementLoaded("#qjh-menu-close-button");
  363. closeButton.onclick = () => {
  364. menuElement.style.display = "none";
  365. };
  366. const toggleInputs = nodeListToArray(document.querySelectorAll("input")).filter((element) => element.getAttribute("qjh-type") === "toggle");
  367. for (const toggleInput of toggleInputs) {
  368. const key = toggleInput.getAttribute("qjh-key");
  369. toggleInput.checked = GM_getValue(key);
  370. toggleInput.onchange = () => {
  371. GM_setValue(key, toggleInput.checked);
  372. };
  373. }
  374. const featButtons = nodeListToArray(document.querySelectorAll("button")).filter((element) => element.getAttribute("qjh-feat-key") !== null);
  375. for (const featButton of featButtons) {
  376. const key = featButton.getAttribute("qjh-feat-key");
  377. const feature = features.find((feature) => feature.key === key);
  378. featButton.onclick = () => {
  379. if (feature.enabled()) {
  380. showMessage(`手动激活功能:${feature.title}`, "green");
  381. feature.task();
  382. }
  383. else {
  384. showMessage(`功能 ${feature.title} 未被启用!`, "red");
  385. }
  386. };
  387. }
  388. }
  389. async function startCourse(courseId) {
  390. const answers = await getCourseAnswers(courseId);
  391. if (answers === null) {
  392. showMessage(`[${courseId}] 无法获取当前课程的答案!`, "red");
  393. return false;
  394. }
  395. else {
  396. location.href = `https://www.2-class.com/courses/exams/${courseId}`;
  397. }
  398. }
  399. async function taskCourses(isSelfCourses) {
  400. if (!isLogined()) {
  401. showMessage("你还没有登录!", "red");
  402. return;
  403. }
  404. let gradeLevels = await (isSelfCourses
  405. ? selfCoursesGradeLevels
  406. : coursesGradeLevels)();
  407. if (gradeLevels === null) {
  408. showMessage(`获取年级名列表失败,功能已中止!`, "red");
  409. return;
  410. }
  411. console.debug("获取总年级名列表", gradeLevels);
  412. gradeLevels = isSelfCourses ? customSelfGradeLevels() : customGradeLevels();
  413. console.debug("已选择的年级列表", gradeLevels);
  414. for (const gradeLevel of gradeLevels) {
  415. const coursesList = isSelfCourses
  416. ? await getSelfCoursesByGradeLevel(gradeLevel)
  417. : await getCoursesByGradeLevel(gradeLevel);
  418. if (coursesList === null) {
  419. showMessage(`[${gradeLevel}] 获取当前年级的课程列表失败,已跳过当前年级!`, "red");
  420. }
  421. const courseIds = coursesList
  422. .filter((it) => !it.isFinish && it.title !== "期末考试")
  423. .map((it) => it.courseId);
  424. if (courseIds.length === 0) {
  425. console.debug(`[${gradeLevel}] 所有${isSelfCourses ? "自学" : ""}课程都是完成状态,已跳过!`);
  426. return;
  427. }
  428. console.debug(`[${gradeLevel}] 未完成的${isSelfCourses ? "自学" : ""}课程`, courseIds);
  429. let committed = 0;
  430. for (const courseId of courseIds) {
  431. if (courseId === "finalExam") {
  432. return;
  433. }
  434. if (!isNone(courseId)) {
  435. const result = await startCourse(courseId);
  436. if (result) {
  437. committed++;
  438. }
  439. else {
  440. console.error(`[${courseId}] 无法提交当前课程,已跳过!`);
  441. }
  442. }
  443. else {
  444. console.error(`[${gradeLevel}] 无法找到 courseId,已跳过!`);
  445. }
  446. }
  447. showMessage(`成功完成了 ${committed} ${isSelfCourses ? "自学" : ""}课程!`, "green");
  448. }
  449. }
  450. async function taskSingleCourse() {
  451. if (!isLogined()) {
  452. showMessage("你还没有登录!", "red");
  453. return;
  454. }
  455. const courseId = location.pathname.match(/(\d+)/g)[0];
  456. const answers = await getCourseAnswers(courseId);
  457. 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, _) => {
  458. const firstAnswer = answers.shift().toString();
  459. return {
  460. type: "index",
  461. answer: firstAnswer,
  462. matchedQuestion: null,
  463. };
  464. }, `答题 [${courseId}]`, answers.length, 50);
  465. const passText = await waitForElementLoaded("#app > div > div.home-container > div > div > div > div.exam-box > div > div > p.exam-pass-title");
  466. if (passText) {
  467. const courses = [];
  468. const courseLevels = customGradeLevels();
  469. for (const courseLevel of courseLevels) {
  470. const result = await getCoursesByGradeLevel(courseLevel);
  471. for (const course of result) {
  472. courses.push(course);
  473. }
  474. }
  475. const courseIds = courses
  476. .filter((it) => !it.isFinish && it.title !== "期末考试")
  477. .map((it) => it.courseId);
  478. if (courseIds.length === 0) {
  479. showMessage("所有的课程已全部自动完成!", "green");
  480. location.href = "https://www.2-class.com/courses/";
  481. }
  482. else {
  483. location.href = `https://www.2-class.com/courses/exams/${courseIds[0]}`;
  484. }
  485. }
  486. }
  487. async function emulateExamination(answers, startButtonSelector, primaryNextButtonSelector, secondaryNextButtonSelector, answerHandler, examinationName, size = 100, interval = 3000, afterStart = async () => { }) {
  488. let isExaminationStarted = false;
  489. let count = 0;
  490. const next = async (nextAnswers, nextButton = null) => {
  491. const questionElement = await waitForElementLoaded(".exam-content-question");
  492. const questionText = removeStuffs(questionElement.innerText.split("\n")[0]);
  493. if (!isExaminationStarted) {
  494. const primaryNextButton = await waitForElementLoaded(primaryNextButtonSelector);
  495. isExaminationStarted = true;
  496. await next(nextAnswers, primaryNextButton);
  497. }
  498. else {
  499. let nextSecButton = nextButton;
  500. if (count > 0) {
  501. nextSecButton = await waitForElementLoaded(secondaryNextButtonSelector);
  502. }
  503. if (!isNone(size) && count < size) {
  504. nextSecButton.onclick = async () => {
  505. setTimeout(async () => await next(nextAnswers, nextSecButton), 0);
  506. };
  507. let { type, answer, matchedQuestion } = answerHandler(answers, questionText);
  508. if (isNone(answer)) {
  509. showMessage(`未找到此题的答案,请手动回答,或等待题库更新:${questionText}`, "red");
  510. count++;
  511. return;
  512. }
  513. else {
  514. const selections = document.getElementsByClassName("exam-single-content-box");
  515. console.debug("选择", answer, selections);
  516. const finalQuestion = matchedQuestion || questionText;
  517. if (!isFullAutomaticEmulationEnabled()) {
  518. showMessage(`${finalQuestion ? finalQuestion + "\n" : ""}第 ${count + 1} 题答案:${type === "index" ? toDisplayAnswer(answer) : answer}`, "green");
  519. }
  520. if (type === "text") {
  521. for (let answerText of answer.split("||")) {
  522. answerText = removeStuffs(answerText);
  523. const selectionElements = htmlCollectionToArray(selections).filter((it) => {
  524. const match = it.innerText.match(/^([A-Z])([.。,,、.])(.*)/);
  525. const answerContent = removeStuffs(match[1 + 2]);
  526. return (!isNone(answerContent) &&
  527. (answerContent === answerText ||
  528. fuzzyMatch(answerContent, answerText).matched));
  529. });
  530. selectionElements.map((it) => it.click());
  531. }
  532. }
  533. else {
  534. for (const answerIndex of answer
  535. .split(",")
  536. .filter((it) => it !== "")
  537. .map((it) => Number(it))) {
  538. const selectionElement = selections[answerIndex];
  539. selectionElement.click();
  540. }
  541. }
  542. if (isFullAutomaticEmulationEnabled()) {
  543. setTimeout(() => nextSecButton.click(), interval);
  544. }
  545. count++;
  546. }
  547. }
  548. }
  549. };
  550. const startButton = await waitForElementLoaded(startButtonSelector);
  551. if (isFullAutomaticEmulationEnabled()) {
  552. showMessage(`自动开始 ${examinationName}!`, "blue");
  553. startButton.click();
  554. await afterStart();
  555. next(answers, null);
  556. }
  557. else {
  558. startButton.onclick = async () => {
  559. showMessage(`开始 ${examinationName}!`, "blue");
  560. await afterStart();
  561. next(answers, null);
  562. };
  563. }
  564. }
  565. async function taskSkip() {
  566. if (!isLogined()) {
  567. showMessage("你还没有登录!", "red");
  568. return;
  569. }
  570. const courseId = location.pathname.match(/(\d+)/g)[0];
  571. const video = (await waitForElementLoaded("#app > div > div.home-container > div > div > div:nth-child(2) > div > div > div > div > div > video"));
  572. 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");
  573. videoControlButton.onclick = () => {
  574. const endTime = video.seekable.end(0);
  575. video.currentTime = endTime;
  576. };
  577. }
  578. async function taskGetCredit() {
  579. if (!isLogined()) {
  580. showMessage("你还没有登录!", "red");
  581. return;
  582. }
  583. const length = 5;
  584. const num = await addMedal();
  585. if (num !== undefined) {
  586. showMessage(`成功领取禁毒徽章 [${num}]!`, "green");
  587. }
  588. else if (num === null) {
  589. showMessage("领取徽章失败!", "red");
  590. }
  591. else {
  592. showMessage("无法领取徽章(可能已领取过),已跳过!", "yellow");
  593. console.warn("无法领取徽章(可能已领取过),已跳过!");
  594. }
  595. const categories = [
  596. { name: "public_good", tag: "read" },
  597. { name: "ma_yun_recommend", tag: "labour" }, // the `ma_yun_recommend` has lots of sub-categorys
  598. { name: "ma_yun_recommend", tag: "movie" },
  599. { name: "ma_yun_recommend", tag: "music" },
  600. { name: "ma_yun_recommend", tag: "physicalEducation" },
  601. { name: "ma_yun_recommend", tag: "arts" },
  602. { name: "ma_yun_recommend", tag: "natural" },
  603. { name: "ma_yun_recommend", tag: "publicWelfareFoundation" },
  604. { name: "school_safe", tag: "safeVolunteer" },
  605. ];
  606. let done = 0;
  607. let failed = 0;
  608. let liked = 0;
  609. for (const category of categories) {
  610. const data = {
  611. categoryName: category.name,
  612. pageNo: 1,
  613. pageSize: 100,
  614. reqtoken: reqtoken(),
  615. tag: category.tag,
  616. };
  617. const resources = await getBeforeResourcesByCategoryName(data);
  618. if (resources === null) {
  619. console.error(`无法获取分类 ${category.name} 的资源,已跳过!`);
  620. continue;
  621. }
  622. console.debug(`获取分类 ${category.name} 的资源`, resources);
  623. for (const resource of resources) {
  624. if (done >= length)
  625. break;
  626. const resourceId = resource.resourceId;
  627. const resourceData = { resourceId, reqtoken: reqtoken() };
  628. const result = await addPCPlayPV(resourceData);
  629. if (result) {
  630. console.debug(`成功完成资源 [${resourceId}]:${resource.title}`);
  631. done++;
  632. }
  633. else {
  634. console.error(`无法完成资源 ${resourceId},已跳过!`);
  635. failed++;
  636. }
  637. const likeResult = await likePC(resourceData);
  638. if (likeResult) {
  639. console.debug(`成功点赞资源 [${resourceId}]!`);
  640. liked++;
  641. }
  642. else {
  643. console.error(`资源点赞失败 [${resourceId}],已跳过!`);
  644. }
  645. }
  646. }
  647. let beforeDone = done;
  648. const checkSuccess = setInterval(() => {
  649. if (done !== 0) {
  650. if (done === beforeDone) {
  651. showMessage(`成功完成 ${done}/${done + failed} 个资源,点赞 ${liked} 个!`, "green");
  652. clearInterval(checkSuccess);
  653. }
  654. else {
  655. beforeDone = done;
  656. }
  657. }
  658. }, 500);
  659. }
  660. async function taskFinalExamination() {
  661. const supportedFinal = libs.supportedFinal;
  662. const gradeLevel = accountGradeLevel();
  663. if (supportedFinal.hasOwnProperty(gradeLevel)) {
  664. const paperName = supportedFinal[gradeLevel];
  665. let papers = libs[paperName];
  666. 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) => {
  667. const [answerList, n] = accurateFind(papers, question) ||
  668. fuzzyFind(papers, question) || [[], 0];
  669. return {
  670. type: "text",
  671. answer: n > 0 ? answerList.map((it) => it.answer).join("||") : null,
  672. matchedQuestion: n > 0 ? answerList.map((it) => it.realQuestion).join("||") : null,
  673. };
  674. }, "期末考试", 10, // 一共 10 道题
  675. 3000 // 默认题目间隔 3s
  676. );
  677. }
  678. else {
  679. showMessage(`你的年级 [${gradeLevel}] 暂未支持期末考试!`, "red");
  680. return;
  681. }
  682. }
  683. async function taskMultiComplete() {
  684. }
  685. async function taskCompetition() {
  686. const supportedCompetition = libs.supportedCompetition;
  687. const gradeLevel = accountGradeLevel();
  688. let gradeGroup;
  689. const gradesPrimary = {
  690. 一年级: 1,
  691. 二年级: 2,
  692. 三年级: 3,
  693. 四年级: 4,
  694. 五年级: 5,
  695. 六年级: 6,
  696. };
  697. if (gradeLevel in gradesPrimary) {
  698. gradeGroup = "小学组";
  699. }
  700. else {
  701. gradeGroup = "中学组";
  702. }
  703. if (supportedCompetition.hasOwnProperty(gradeGroup)) {
  704. showMessage(`已自动选择 [${gradeGroup}] 知识竞赛题库`, "cornflowerblue");
  705. const paperName = supportedCompetition[gradeGroup];
  706. const papers = libs[paperName];
  707. if (!Array.isArray(papers)) {
  708. showMessage(`[${gradeGroup}] 暂不支持知识竞赛!`, "red");
  709. return;
  710. }
  711. 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) => {
  712. const [answerList, n] = accurateFind(papers, question) ||
  713. fuzzyFind(papers, question) || [[], 0];
  714. return {
  715. type: "text",
  716. answer: n > 0 ? answerList.map((it) => it.answer).join("||") : null,
  717. matchedQuestion: n > 0 ? answerList.map((it) => it.realQuestion).join("||") : null,
  718. };
  719. }, "知识竞赛", 20, // 最大题目数,竞赛只有 20 道题目,如果未定义并打开了 `自动下一题并提交` 会导致循环提示最后一题 80 次
  720. 3000, // 与下一题的间隔时间,单位毫秒,默认 3 秒
  721. async () => {
  722. const gradeGroupDialog = await waitForElementLoaded("#app > div > div.home-container > div > div > div.competiotion-exam-box-all > div.dialog-mask > div");
  723. const options = nodeListToArray(gradeGroupDialog.querySelectorAll(".option"));
  724. const filteredOptions = options.filter((it) => it.innerHTML === gradeGroup);
  725. const resultOption = filteredOptions[0];
  726. if (filteredOptions.length < 1 || isNone(resultOption)) {
  727. showMessage(`[${gradeGroup}] 暂不支持知识竞赛!`, "red");
  728. return;
  729. }
  730. else {
  731. resultOption.click();
  732. }
  733. });
  734. }
  735. else {
  736. showMessage(`你的年级 [${gradeLevel}] 暂未支持知识竞赛!`, "red");
  737. return;
  738. }
  739. }
  740. function showMessage(text, color) {
  741. Toastify({
  742. text,
  743. duration: toastifyDuration,
  744. newWindow: true,
  745. gravity: toastifyGravity,
  746. position: toastifyPosition,
  747. stopOnFocus: true,
  748. style: { background: color },
  749. }).showToast();
  750. }
  751. function featureNotAvailable(name = "(未知)") {
  752. showMessage(`${name} 功能当前不可用,请尝试刷新页面。如果问题依旧请上报这个 bug!`, "red");
  753. }
  754. function isNone(obj) {
  755. return obj == undefined || obj == null;
  756. }
  757. function getGMValue(name, defaultValue) {
  758. let value = GM_getValue(name);
  759. if (isNone(value)) {
  760. value = defaultValue;
  761. GM_setValue(name, defaultValue);
  762. }
  763. return value;
  764. }
  765. async function waitForElementLoaded(querySelector) {
  766. return new Promise((resolve, reject) => {
  767. let attempts = 0;
  768. const tryFind = () => {
  769. const element = document.querySelector(querySelector);
  770. if (element) {
  771. resolve(element);
  772. }
  773. else {
  774. attempts++;
  775. if (attempts >= 30) {
  776. console.error(`无法找到元素 [${querySelector}],已放弃!`);
  777. reject();
  778. }
  779. else {
  780. setTimeout(tryFind, 250 * Math.pow(1.1, attempts));
  781. }
  782. }
  783. };
  784. tryFind();
  785. });
  786. }
  787. function removeStuffs(string) {
  788. return isNone(string)
  789. ? null
  790. : string
  791. .replace(/\s*/g, "")
  792. .replace(/[,。?!;:—【】(),.?!;:-\[\]\(\)]/g, "");
  793. }
  794. function toDisplayAnswer(answer) {
  795. const alphas = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
  796. let result = "";
  797. for (const singleAnswer of answer.split(",")) {
  798. const index = Number(singleAnswer);
  799. result = result + alphas[index];
  800. }
  801. return result;
  802. }
  803. function nodeListToArray(nodeList) {
  804. return Array.prototype.slice.call(nodeList);
  805. }
  806. function htmlCollectionToArray(htmlCollection) {
  807. const result = [];
  808. for (const element of htmlCollection)
  809. result.push(element);
  810. return result;
  811. }
  812. function arrayDiff(array1, array2) {
  813. return array1.concat(array2).filter((v, _, array) => {
  814. return array.indexOf(v) === array.lastIndexOf(v);
  815. });
  816. }
  817. function fuzzyMatch(a, b) {
  818. const aChars = a.split("");
  819. const bChars = b.split("");
  820. const length = aChars.length > bChars.length ? aChars.length : bChars.length;
  821. const diff = arrayDiff(aChars, bChars);
  822. const diffLength = diff.length;
  823. const unconfidence = diffLength / length;
  824. return {
  825. matched: 1 - unconfidence >= fuzzyFindConfidenceTreshold,
  826. confidence: 1 - unconfidence,
  827. };
  828. }
  829. function accurateFind(papers, question) {
  830. const results = papers.filter((it) => removeStuffs(it.question) === removeStuffs(question));
  831. if (results.length > 0) {
  832. console.debug(`精确匹配问题:${question} ${question}`);
  833. return [
  834. results.map((it) => {
  835. return { answer: it.answer, realQuestion: it.question };
  836. }),
  837. results.length,
  838. ];
  839. }
  840. else {
  841. return null;
  842. }
  843. }
  844. function fuzzyFind(papers, question) {
  845. const chars = question.split("");
  846. const length = chars.length;
  847. const percentages = [];
  848. for (const paper of papers) {
  849. const { matched, confidence } = fuzzyMatch(question, paper.question);
  850. if (matched) {
  851. percentages.push({
  852. question: paper.question,
  853. answer: paper.answer,
  854. confidence,
  855. });
  856. }
  857. }
  858. const theMostConfidents = percentages
  859. .filter((it) => it.confidence > 0)
  860. .sort((a, b) => a.confidence - b.confidence);
  861. if (theMostConfidents.length <= 0) {
  862. console.error(`模糊匹配未找到高度匹配的结果:${question}`);
  863. return null;
  864. }
  865. console.debug(`模糊匹配问题:${question} ${theMostConfidents
  866. .map((it) => `(${it.confidence})${it.question}`)
  867. .join("||")}`);
  868. return [
  869. theMostConfidents.map((it) => {
  870. return { answer: it.answer, realQuestion: it.question };
  871. }),
  872. theMostConfidents.length,
  873. ];
  874. }
  875. async function insertValue(input, value) {
  876. input.value = value;
  877. const event = new Event("input", {
  878. bubbles: true,
  879. });
  880. const tracker = input._valueTracker;
  881. event.simulated = true;
  882. if (tracker) {
  883. tracker.setValue(value);
  884. }
  885. input.dispatchEvent(event);
  886. }
  887. async function login(account, password) {
  888. 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");
  889. loginButton.click();
  890. const accountInput = (await waitForElementLoaded("#account"));
  891. const passwordInput = (await waitForElementLoaded("#password"));
  892. passwordInput.type = "text";
  893. 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");
  894. await new Promise((resolve) => setTimeout(resolve, 500));
  895. await insertValue(accountInput, account);
  896. await insertValue(passwordInput, password);
  897. submitButton.click();
  898. waitForElementLoaded("#login_nc")
  899. .then(async () => {
  900. showMessage("正在进行模拟滑块验证,请稍等...", "green");
  901. await mockVerify();
  902. waitForElementLoaded("div > div > div > div.ant-notification-notice-description").then(() => {
  903. showMessage("检测到滑块验证登入失败,请重新刷新网页并确保开发者工具处于开启状态!", "red");
  904. });
  905. })
  906. .catch(() => {
  907. console.log("无滑块验证出现,已直接登入");
  908. });
  909. }
  910. async function mockVerify() {
  911. const mockDistance = 394; // 滑块验证的长度
  912. const mockInterval = 20; // 滑动间隔
  913. const mockButtonId = "nc_1_n1z"; // 滑块验证的可交互按钮 ID
  914. const verifyButton = document.getElementById(mockButtonId);
  915. const clientRect = verifyButton.getBoundingClientRect();
  916. const x = clientRect.x;
  917. const y = clientRect.y;
  918. const mousedown = new MouseEvent("mousedown", {
  919. bubbles: true,
  920. cancelable: true,
  921. clientX: x,
  922. clientY: y,
  923. });
  924. verifyButton.dispatchEvent(mousedown);
  925. let dx = 0;
  926. let dy = 0;
  927. const timer = setInterval(function () {
  928. const _x = x + dx;
  929. const _y = y + dy;
  930. const mousemoveEvent = new MouseEvent("mousemove", {
  931. bubbles: true,
  932. cancelable: true,
  933. clientX: _x,
  934. clientY: _y,
  935. });
  936. verifyButton.dispatchEvent(mousemoveEvent);
  937. if (_x - x >= mockDistance) {
  938. clearInterval(timer);
  939. const mouseupEvent = new MouseEvent("mouseup", {
  940. bubbles: true,
  941. cancelable: true,
  942. clientX: _x,
  943. clientY: _y,
  944. });
  945. verifyButton.dispatchEvent(mouseupEvent);
  946. }
  947. else {
  948. dx += Math.ceil(Math.random() * 50);
  949. }
  950. }, mockInterval);
  951. }
  952. const container = document.createElement("div");
  953. container.setAttribute("id", "qjh-menu");
  954. container.innerHTML = `<style>
  955. .qjh-menu {
  956. height: max-content;
  957. box-shadow: 1px 1px 10px #909090;
  958. padding: 1em;
  959. position: fixed;
  960. z-index: 999;
  961. right: 1%;
  962. top: 3%;
  963. width: 25%;
  964. -webkit-border-radius: 10px;
  965. -moz-border-radius: 10px;
  966. border-radius: 10px;
  967. }
  968.  
  969. .form-inline {
  970. display: inline-block;
  971. }
  972. </style>
  973.  
  974. <div class="card container qjh-menu">
  975. <div class="card-header">
  976. <div class="card-title text-bold h5" id="qjh-menu-title">
  977. QingJiaoHelper
  978. <button
  979. class="btn btn-link float-right"
  980. type="button"
  981. id="qjh-menu-close-button"
  982. >
  983. </button>
  984. </div>
  985. </div>
  986.  
  987. <div class="card-body">
  988. <div class="toast toast-warning">
  989. ⚠注意:勾选的功能会在下一次刷新页面时<mark><b>自动激活</b></mark
  990. >,未勾选的功能只能手动启用!点击<b>一键完成</b>按钮可以在这个菜单中直接完成,而不用手动跳转到对应页面。
  991. </div>
  992.  
  993. <div class="divider text-center" data-content="考试"></div>
  994.  
  995. <div class="form-group">
  996. <label class="form-switch">
  997. <b>期末考试</b>
  998. <input
  999. type="checkbox"
  1000. qjh-type="toggle"
  1001. qjh-key="qjh_isTaskFinalExaminationEnabled"
  1002. />
  1003. <i class="form-icon"></i>
  1004. <button class="btn btn-sm mx-2" type="button">
  1005. <a href="/courses/exams/finalExam">点击跳转</a>
  1006. </button>
  1007. </label>
  1008. </div>
  1009.  
  1010. <div class="form-group">
  1011. <label class="form-switch">
  1012. <b>知识竞赛</b>
  1013. <input
  1014. type="checkbox"
  1015. qjh-type="toggle"
  1016. qjh-key="qjh_isTaskCompetitionEnabled"
  1017. />
  1018. <i class="form-icon"></i>
  1019. <button class="btn btn-sm mx-2" type="button">
  1020. <a href="/competition">点击跳转</a>
  1021. </button>
  1022. </label>
  1023. </div>
  1024.  
  1025. <div class="divider text-center" data-content="课程"></div>
  1026.  
  1027. <div>
  1028. <div class="form-group" id="qjh-menu-feat-courses">
  1029. <label class="form-switch">
  1030. <b>完成所选年级的课程</b>
  1031. <input
  1032. type="checkbox"
  1033. qjh-type="toggle"
  1034. qjh-key="qjh_isTaskCoursesEnabled"
  1035. />
  1036. <i class="form-icon"></i>
  1037. <button class="btn btn-sm mx-2" type="button" qjh-feat-key="courses">
  1038. 一键完成👉
  1039. </button>
  1040. </label>
  1041. </div>
  1042.  
  1043. <div class="form-group" id="qjh-menu-feat-self-courses">
  1044. <label class="form-switch">
  1045. <b>完成所选年级的自学课程</b>
  1046. <input
  1047. type="checkbox"
  1048. qjh-type="toggle"
  1049. qjh-key="qjh_isTaskSelfCourseEnabled"
  1050. />
  1051. <i class="form-icon"></i>
  1052. <button
  1053. class="btn btn-sm mx-2"
  1054. type="button"
  1055. qjh-feat-key="selfCourse"
  1056. >
  1057. 一键完成👉
  1058. </button>
  1059. </label>
  1060. </div>
  1061.  
  1062. <div class="divider text-center" data-content="其他"></div>
  1063.  
  1064. <div class="form-group"></div>
  1065. <label class="form-switch">
  1066. <b>获取每日学分(点赞视频和领取徽章)</b>
  1067. <input
  1068. type="checkbox"
  1069. qjh-type="toggle"
  1070. qjh-key="qjh_isTaskGetCreditEnabled"
  1071. />
  1072. <i class="form-icon"></i>
  1073. <button
  1074. class="btn btn-sm mx-2"
  1075. type="button"
  1076. onclick="taskGetCredit"
  1077. qjh-feat-key="credit"
  1078. >
  1079. 一键完成👉
  1080. </button>
  1081. </label>
  1082. </div>
  1083.  
  1084. <div class="form-group">
  1085. <label class="form-switch">
  1086. <b>自动开始作答、下一题和提交</b>
  1087. <input
  1088. type="checkbox"
  1089. qjh-type="toggle"
  1090. qjh-key="qjh_isFullAutomaticEmulationEnabled"
  1091. />
  1092. <i class="form-icon"></i>
  1093. </label>
  1094. </div>
  1095.  
  1096. </div>
  1097.  
  1098. <div class="divider"></div>
  1099.  
  1100. <div class="card-footer text-gray">
  1101. 本脚本由 FoliageOwO
  1102. <b><a href="https://www.gnu.org/licenses/gpl-3.0.en.html">GPL-3.0</a></b>
  1103. 开源许可在 GitHub 开源,脚本地址:<a
  1104. href="https://github.com/FoliageOwO/QingJiaoHelper"
  1105. target="_blank"
  1106. >GitHub</a
  1107. >、<a
  1108. href="https://greasyfork.org/zh-CN/scripts/452984-qingjiaohelper"
  1109. target="_blank"
  1110. >GreasyFork</a
  1111. >。
  1112. </div>
  1113. </div>
  1114. </div>
  1115. `;
  1116. container.style.display = "none";
  1117. document.body.appendChild(container);
  1118. function showMenu() {
  1119. container.style.display = "unset";
  1120. }