华师网络教育学习助手

华师网络教育学习助手,自动学习、获取题库

  1. // ==UserScript==
  2. // @name 华师网络教育学习助手
  3. // @version 0.0.5
  4. // @namespace http://tampermonkey.net/
  5. // @description 华师网络教育学习助手,自动学习、获取题库
  6. // @author 4Ark
  7. // @match *https://gdou.scnu.edu.cn/learnspace/learn/learn/blue/index.action*
  8. // @match *https://scnu-exam.webtrn.cn/platformwebapi/student/exam/studentExam_queryExamInfo.action*
  9. // @match *https://gdou.scnu.edu.cn/learnspace/learn/learn/blue/exam_checkPaperToexam.action*
  10. // @match *https://scnu-exam.webtrn.cn/student/exam/studentExam_studentInfo.action*
  11. // @match *https://scnu-exam.webtrn.cn/exam/examflow_index.action*
  12. // @run-at document-end
  13. // @grant unsafeWindow
  14. // @grant GM_xmlhttpRequest
  15. // @grant GM_setValue
  16. // @grant GM_getValue
  17. // @grant GM_info
  18. // @grant GM_setClipboard
  19. // @antifeature ads
  20. // @license MIT
  21. // ==/UserScript==
  22.  
  23. ;(function () {
  24. 'use strict'
  25.  
  26. const DEFAULT_AUTO_GET_EXAM_COUNT = 15
  27.  
  28. const PLAY_SPEED_STRATEGY = {
  29. 25: 16,
  30. 15: 12,
  31. 10: 8,
  32. 3: 4,
  33. 2: 2,
  34. 1: 1
  35. }
  36.  
  37. const EXAM_IDS_KEY = 'huashi-exam-ids'
  38.  
  39. const QUESTION_TYPE_SORT = {
  40. 单项选择题: 1,
  41. 多项选择题: 2,
  42. 判断题: 3,
  43. 填空题: 4
  44. }
  45.  
  46. const UTILS_TYPE = {
  47. LEARN: '自动学习',
  48. GET_EXAM: '获取题库'
  49. }
  50.  
  51. let util_type = UTILS_TYPE.LEARN
  52.  
  53. const CHAPTER_TYPE = {
  54. VIDEO: 'video',
  55. TEXT: 'text'
  56. }
  57.  
  58. const GET_EXAM_BASE_URL =
  59. 'https://scnu-exam.webtrn.cn/platformwebapi/student/exam/studentExam_queryExamInfo.action'
  60. const GET_EXAM_BASE_URL_2 =
  61. 'https://gdou.scnu.edu.cn/learnspace/learn/learn/blue/exam_checkPaperToexam.action'
  62.  
  63. const SURE_EXAM_BASE_URL =
  64. 'https://scnu-exam.webtrn.cn/student/exam/studentExam_studentInfo.action'
  65.  
  66. const EXAM_BASE_URL = 'https://scnu-exam.webtrn.cn/exam/examflow_index.action'
  67.  
  68. let currentPage
  69. let currentFrame
  70.  
  71. const delay = (time) =>
  72. new Promise((resolve) => {
  73. setTimeout(() => resolve(), time)
  74. })
  75.  
  76. function main() {
  77. const getType = function () {
  78. const module = getCurrentModule()
  79.  
  80. if (module.includes('教学视频')) {
  81. return UTILS_TYPE.LEARN
  82. }
  83.  
  84. if (module.includes('在线练习')) {
  85. return UTILS_TYPE.GET_EXAM
  86. }
  87. }
  88.  
  89. $('.nav .nav_li').click(function () {
  90. var tab = $(this).find('a').text()
  91.  
  92. if (tab === '课程内容') {
  93. initToolbar()
  94.  
  95. awaitPageLoaded(function () {
  96. setMessage('准备中...')
  97.  
  98. awaitFrameLoaded(() => {
  99. const type = getType()
  100.  
  101. const actions = {
  102. [UTILS_TYPE.LEARN]: startLearn,
  103. [UTILS_TYPE.GET_EXAM]: getExam
  104. }
  105.  
  106. setUtilType(type)
  107.  
  108. actions[type] && actions[type]()
  109. })
  110. })
  111.  
  112. return
  113. }
  114.  
  115. $('#huashi-exam-toolbar').remove()
  116. })
  117.  
  118. if (location.href.includes(GET_EXAM_BASE_URL)) {
  119. openExam()
  120. }
  121.  
  122. if (location.href.includes(GET_EXAM_BASE_URL_2)) {
  123. getExam()
  124. }
  125.  
  126. if (location.href.includes(SURE_EXAM_BASE_URL)) {
  127. sureExam()
  128. }
  129.  
  130. if (location.href.includes(EXAM_BASE_URL)) {
  131. submitExam()
  132. }
  133. }
  134.  
  135. function initToolbar() {
  136. if ($('#huashi-exam-toolbar').length) return
  137.  
  138. const messageBox = $(
  139. `<div id="huashi-exam-toolbar">
  140. <p style="color:red">华师网络教育学习助手</p>
  141. <div id="huashi-exam-message"></div>
  142. <div id="huashi-exam-util-type">当前功能:<span>${util_type}</span></div>
  143. </div>`
  144. )
  145.  
  146. const css = `
  147. #huashi-exam-toolbar {
  148. position: absolute;
  149. width: 100%;
  150. height: 50px;
  151. padding: 0px 20px;
  152. background: rgb(255, 255, 255);
  153. top: 0px; left: 0px;
  154. color: red;
  155. display: flex;
  156. justify-content: space-between;
  157. align-items: center;
  158. box-sizing: border-box;
  159. }
  160. #huashi-exam-message {
  161. color: red;
  162. }
  163. `
  164.  
  165. $('body').append(messageBox)
  166. $('head').append($('<style type="text/css">').html(css))
  167. }
  168.  
  169. function getCurrentModule() {
  170. return $page('#nav .vtitle span.v2').text()
  171. }
  172.  
  173. function $page(selector, context = currentPage) {
  174. return $(context).contents().find(selector)
  175. }
  176.  
  177. function setMessage(message) {
  178. $('#huashi-exam-message').text(message)
  179. }
  180.  
  181. function setUtilType(type) {
  182. util_type = type
  183.  
  184. $('#huashi-exam-util-type span').text(util_type)
  185.  
  186. setMessage('')
  187. }
  188.  
  189. function awaitPageLoaded(cb) {
  190. const page = $('#mainContent')
  191.  
  192. $(page).one('load', function () {
  193. currentPage = $(this)
  194.  
  195. cb()
  196. })
  197. }
  198.  
  199. function awaitFrameLoaded(cb) {
  200. $(currentPage)
  201. .contents()
  202. .find('#mainFrame')
  203. .on('load', function () {
  204. currentFrame = $(this)
  205.  
  206. cb()
  207. })
  208. }
  209.  
  210. function fmtMSS(s) {
  211. s = parseInt(s)
  212.  
  213. if (s < 0 || isNaN(s)) return ''
  214.  
  215. return (s - (s %= 60)) / 60 + (s > 9 ? ':' : ':0') + s
  216. }
  217.  
  218. function fmtM(s) {
  219. s = parseInt(s)
  220.  
  221. return (s - (s %= 60)) / 60
  222. }
  223.  
  224. function startLearn() {
  225. const getSpeed = function (minute) {
  226. const key = Object.keys(PLAY_SPEED_STRATEGY)
  227. .sort((a, b) => b - a)
  228. .find(function (m) {
  229. return m <= minute
  230. })
  231.  
  232. return PLAY_SPEED_STRATEGY[key] || 1
  233. }
  234.  
  235. const passVideo = function (video, next) {
  236. const duration = fmtMSS(video.duration)
  237. const minute = fmtM(video.duration)
  238.  
  239. const speed = getSpeed(minute)
  240.  
  241. setMessage(
  242. '当前视频时长:' +
  243. duration +
  244. ',将采用' +
  245. speed +
  246. '倍速播放,预计需要' +
  247. (minute * (speed / 100)).toFixed(1) +
  248. '分钟。'
  249. )
  250.  
  251. video.muted = true
  252. video.playbackRate = speed
  253.  
  254. const timer = setInterval(function () {
  255. if (video && video.playbackRate !== speed) {
  256. video.playbackRate = speed
  257. }
  258. }, 5000)
  259.  
  260. $(video).on('ended', function () {
  261. clearInterval(timer)
  262.  
  263. next()
  264. })
  265. }
  266.  
  267. const passChapterByType = function (type, context) {
  268. const next = getNext()
  269.  
  270. if (type === CHAPTER_TYPE.TEXT) {
  271. return next()
  272. }
  273.  
  274. if (type === CHAPTER_TYPE.VIDEO) {
  275. return passVideo(context, next)
  276. }
  277. }
  278.  
  279. const getNext = function () {
  280. const menus = $page('.menuct .menub')
  281. const menu = $page('.menuct .menubu')
  282.  
  283. const index = menus.index(menu)
  284.  
  285. const nextChapter = function () {
  286. const chapters = $page('.vcon:visible .vconlist > li')
  287. const chapter = $page('.vcon:visible .vconlist > li.select')
  288.  
  289. const index = chapters.index(chapter)
  290.  
  291. if (index === chapters.length - 1) {
  292. return () => {
  293. setMessage('学习完毕')
  294. }
  295. }
  296.  
  297. return function () {
  298. setMessage('正在加载...')
  299.  
  300. setTimeout(() => {
  301. $(chapters[index + 1]).click()
  302. $(chapters[index + 1])
  303. .find('a')
  304. .click()
  305. }, 500)
  306. }
  307. }
  308.  
  309. if (index === menus.length - 1) {
  310. return nextChapter()
  311. }
  312.  
  313. return function () {
  314. setMessage('正在加载...')
  315.  
  316. $page('#rtarr').click()
  317. }
  318. }
  319.  
  320. const awaitVideoLoaded = function (cb) {
  321. var target = $(currentFrame).contents().find('body')[0]
  322.  
  323. const textContent = $(target).find('#textContent')
  324.  
  325. if (textContent.length >= 1) {
  326. cb(CHAPTER_TYPE.TEXT, textContent)
  327.  
  328. return
  329. }
  330.  
  331. let loadedVideo
  332.  
  333. var observer = new MutationObserver(function (mutations) {
  334. mutations.forEach(function () {
  335. if (loadedVideo) return
  336.  
  337. const video = $(target).find('.cont video')
  338.  
  339. if (video.attr('src')) {
  340. loadedVideo = true
  341.  
  342. video.on('loadedmetadata', function () {
  343. cb(CHAPTER_TYPE.VIDEO, video[0])
  344. })
  345.  
  346. observer.disconnect()
  347. }
  348. })
  349. })
  350.  
  351. observer.observe(target, { subtree: true, childList: true })
  352. }
  353.  
  354. awaitVideoLoaded((type, context) => {
  355. setTimeout(() => {
  356. passChapterByType(type, context)
  357. }, 500)
  358. })
  359. }
  360.  
  361. function getExam() {
  362. setUtilType(UTILS_TYPE.GET_EXAM)
  363.  
  364. const isSolo = location.href.includes(GET_EXAM_BASE_URL_2)
  365.  
  366. if (!isSolo) {
  367. const isConfirm = window.confirm('是否需要自动获取题库?')
  368.  
  369. if (!isConfirm) return
  370. }
  371.  
  372. const autoGetExamCount = window.prompt(
  373. '请输入自动获取题库次数:',
  374. DEFAULT_AUTO_GET_EXAM_COUNT
  375. )
  376.  
  377. GM_setValue('autoGetExamCount', autoGetExamCount)
  378.  
  379. GM_setValue(EXAM_IDS_KEY, [])
  380.  
  381. if (isSolo) {
  382. window.open($('#examIframe', currentFrame).attr('src'))
  383. } else {
  384. window.open($page('#examIframe', currentFrame).attr('src'))
  385. }
  386. }
  387.  
  388. async function openExam() {
  389. if (self != top) {
  390. return
  391. }
  392.  
  393. const ids = GM_getValue(EXAM_IDS_KEY) || []
  394.  
  395. console.log('4ark ids -->', ids)
  396.  
  397. const count = GM_getValue('autoGetExamCount') || DEFAULT_AUTO_GET_EXAM_COUNT
  398.  
  399. console.log('4ark count -->', count)
  400.  
  401. if (ids.length >= count) {
  402. initToolbar()
  403.  
  404. setUtilType(UTILS_TYPE.GET_EXAM)
  405.  
  406. setMessage('正在下载题库...')
  407.  
  408. return downloadExam(ids)
  409. }
  410.  
  411. await delay(500)
  412.  
  413. console.log(`4ark $('#viewRecordBtn').length`, $('#viewRecordBtn').length)
  414.  
  415. if ($('#viewRecordBtn').length) {
  416. GM_setValue(EXAM_IDS_KEY, [
  417. ...ids,
  418. $('#viewRecordBtn').attr('href').split('=').pop()
  419. ])
  420. }
  421. ;``
  422.  
  423. await delay(500)
  424.  
  425. $('#goBtn').click()
  426.  
  427. await delay(500)
  428.  
  429. $('.TB_command_btn ').eq(1).click()
  430. }
  431.  
  432. async function sureExam() {
  433. const isExercise = $('.exam-primTit').text().includes('在线练习')
  434.  
  435. if (!isExercise) return
  436.  
  437. await delay(500)
  438.  
  439. console.log('4ark', $('.submit_solid'))
  440.  
  441. $('.submit_solid').click()
  442.  
  443. await delay(500)
  444.  
  445. $('.TB_command_btn ').eq(1).click()
  446. }
  447.  
  448. async function submitExam() {
  449. const isExercise = $('.paper_name').text().includes('在线练习')
  450.  
  451. if (!isExercise) return
  452.  
  453. await delay(500)
  454.  
  455. $('.paper_submit').click()
  456.  
  457. await delay(500)
  458.  
  459. $('.win_btn1').eq(1).click()
  460.  
  461. await delay(500)
  462.  
  463. $('.TB_command_btn').click()
  464. }
  465.  
  466. async function downloadExam(ids) {
  467. const tasks = ids.map((id) => requestExam(id))
  468.  
  469. const result = await Promise.all(tasks)
  470.  
  471. const html = getHTML(result)
  472.  
  473. const frag = document.createDocumentFragment()
  474.  
  475. const div = $(`<div id="huashi-exam-download-div">${html}</div>`)
  476.  
  477. div.css({
  478. display: 'none'
  479. })
  480.  
  481. frag.appendChild(div[0])
  482.  
  483. $('body').append(frag)
  484.  
  485. const download = function (content, filename) {
  486. var eleLink = document.createElement('a')
  487. eleLink.download = filename
  488. eleLink.style.display = 'none'
  489. // 字符内容转变成blob地址
  490. var blob = new Blob([content])
  491. eleLink.href = URL.createObjectURL(blob)
  492. // 触发点击
  493. document.body.appendChild(eleLink)
  494. eleLink.click()
  495. // 然后移除
  496. document.body.removeChild(eleLink)
  497. }
  498.  
  499. const downloadHTML = function (questions) {
  500. const html = questions
  501. .map((question, index) => {
  502. return `<body style="padding: 32px;"><div class="question" style="margin-bottom: 20px;">
  503. <div class="question-content" style="display: flex;">${
  504. index + 1
  505. }、${
  506. question.content?.src
  507. ? `<img src="${question.content.src}" style="margin-bottom: 20px;" />`
  508. : question.content
  509. }</div>
  510. <div class="question-options" style="padding-left: 20px;">${question.options
  511. .map((option) => `<li>${option}</li>`)
  512. .join('')}</div>
  513. <div class="question-answer" style="padding-left: 20px; margin-top: 10px;">参考答案:${
  514. question.answer?.src
  515. ? `<img src="${question.answer.src}" style="margin-bottom: 20px;" />`
  516. : question.answer
  517. }</div>
  518. </div></body>`
  519. })
  520. .join('')
  521.  
  522. download(html, $('.mod_tit h2').text().trim() + '题库.html')
  523.  
  524. GM_setValue(EXAM_IDS_KEY, [])
  525.  
  526. setMessage('下载完成')
  527. }
  528.  
  529. const getQuestions = function () {
  530. let questions = $('.q_content')
  531. .map((_, el) => {
  532. let content = $(el)
  533. .find('.divQuestionTitle')
  534. .text()
  535. .replace(/\d+、/, '')
  536.  
  537. const img = $(el).find('.divQuestionTitle img').attr('src')
  538.  
  539. if (img) {
  540. content = {
  541. src: img.startsWith('https')
  542. ? img
  543. : `https://scnu-exam.webtrn.cn${img}`
  544. }
  545. }
  546.  
  547. const options = $(el)
  548. .find('.q_option_readonly')
  549. .map(function () {
  550. return $(this).text()
  551. })
  552. .get()
  553. let answer =
  554. $(el).find('.exam_rightAnswer span[name=rightAnswer]').text() ||
  555. $(el).find('.exam_rightAnswer .has_standard_answer').text()
  556.  
  557. const answerImg = $(el)
  558. .find('.exam_rightAnswer .has_standard_answer img')
  559. .attr('src')
  560.  
  561. if (answerImg) {
  562. answer = {
  563. src: answerImg.startsWith('https')
  564. ? answerImg
  565. : `https://scnu-exam.webtrn.cn${answerImg}`
  566. }
  567. }
  568.  
  569. return {
  570. content,
  571. options,
  572. answer
  573. }
  574. })
  575. .get()
  576.  
  577. const arrayUniqueByKey = (array, key) => {
  578. return [...new Map(array.map((item) => [item[key], item])).values()]
  579. }
  580.  
  581. questions = arrayUniqueByKey(questions, 'content')
  582.  
  583. downloadHTML(questions)
  584. }
  585.  
  586. getQuestions()
  587. }
  588.  
  589. function requestExam(id) {
  590. return new Promise((resolve) => {
  591. $.ajax({
  592. url: 'https://scnu-exam.webtrn.cn/student/exam/examrecord_getRecordPaperStructure.action',
  593. type: 'POST',
  594. headers: {
  595. Authority: 'scnu-exam.webtrn.cn',
  596. Pragma: 'no-cache',
  597. 'Cache-Control': 'no-cache',
  598. Accept: '*/*',
  599. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
  600. 'X-Requested-With': 'XMLHttpRequest',
  601. 'User-Agent':
  602. 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.47',
  603. Origin: 'https://scnu-exam.webtrn.cn',
  604. 'Sec-Fetch-Site': 'same-origin',
  605. 'Sec-Fetch-Mode': 'cors',
  606. 'Sec-Fetch-Dest': 'empty',
  607. Referer: `https://scnu-exam.webtrn.cn/student/exam/examrecord_recordDetail.action?recordId=${id}`,
  608. 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
  609. Cookie: document.cookie,
  610. 'Accept-Encoding': 'gzip'
  611. },
  612. contentType: 'application/x-www-form-urlencoded',
  613. data: {
  614. recordId: id
  615. }
  616. }).done(function (data) {
  617. data = JSON.parse(data)
  618.  
  619. const { contentList, examBatchId } = data.data
  620.  
  621. const contentIds = contentList.map(({ id }) => id).join()
  622.  
  623. return $.ajax({
  624. type: 'POST',
  625. url: 'https://scnu-exam.webtrn.cn/student/exam/examrecord_getRecordContent.action',
  626. headers: {
  627. Authority: 'scnu-exam.webtrn.cn',
  628. Pragma: 'no-cache',
  629. 'Cache-Control': 'no-cache',
  630. Accept: '*/*',
  631. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
  632. 'X-Requested-With': 'XMLHttpRequest',
  633. 'User-Agent':
  634. 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.47',
  635. Origin: 'https://scnu-exam.webtrn.cn',
  636. 'Sec-Fetch-Site': 'same-origin',
  637. 'Sec-Fetch-Mode': 'cors',
  638. 'Sec-Fetch-Dest': 'empty',
  639. Referer: `https://scnu-exam.webtrn.cn/student/exam/examrecord_recordDetail.action?recordId=${id}`,
  640. 'Accept-Language':
  641. 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
  642. Cookie: document.cookie,
  643. 'Accept-Encoding': 'gzip'
  644. },
  645. contentType: 'application/x-www-form-urlencoded',
  646. data: {
  647. recordId: id,
  648. examBatchId: examBatchId,
  649. contentIds: contentIds,
  650. 'params.monitor': '',
  651. 'params.isRandomQuestion': '0'
  652. }
  653. }).then((data) => {
  654. data = JSON.parse(data)
  655.  
  656. contentList.forEach((item) => {
  657. if (data?.data?.[item.id]) {
  658. data.data[item.id].type = item.name
  659. }
  660. })
  661.  
  662. if (data.data) {
  663. resolve(data.data)
  664. } else {
  665. resolve({})
  666. }
  667. })
  668. })
  669. })
  670. }
  671.  
  672. function getHTML(data) {
  673. return data
  674. .map((item) => {
  675. return Object.values(item)
  676. .map((val) => {
  677. if (val.contentHtml) return val
  678. })
  679. .filter((s) => s)
  680. })
  681. .flat()
  682. .sort((a, b) =>
  683. QUESTION_TYPE_SORT[a.type] > QUESTION_TYPE_SORT[b.type] ? 1 : -1
  684. )
  685. .map((val) => val.contentHtml)
  686. .join('')
  687. }
  688.  
  689. main()
  690. })()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址