好医生-视频一键到底与自动答题

提供好医生CME继续医学教育平台的视频倍速与一键看完, 并且支持考试一键完成, 现已支持: 北京健康在线-好医生继续医学教育(包含河北地区)、北京市继续医学教育必修课培训2024(北京市全员必修课培训)。原作者为limkim。目前已经将代码删除,不在维护。我已将代码进行fork,在发布上来,不知道什么时候会不能用。如果不能用我将尝试修复,但是不一定能修好,且用且珍惜。

  1. // ==UserScript==
  2. // @name 好医生-视频一键到底与自动答题
  3. // @namespace https://github.com/houziyu/cme-haoyisheng-helper
  4. // @version 1.5.4
  5. // @description 提供好医生CME继续医学教育平台的视频倍速与一键看完, 并且支持考试一键完成, 现已支持: 北京健康在线-好医生继续医学教育(包含河北地区)、北京市继续医学教育必修课培训2024(北京市全员必修课培训)。原作者为limkim。目前已经将代码删除,不在维护。我已将代码进行fork,在发布上来,不知道什么时候会不能用。如果不能用我将尝试修复,但是不一定能修好,且用且珍惜。
  6. // @author limkim
  7. // @require https://cdn.jsdelivr.net/npm/sweetalert2@11
  8. // @match *://cme.haoyisheng.com/cme/polyv.jsp*
  9. // @match *://cme.haoyisheng.com/cme/study2.jsp*
  10. // @match *://cme.haoyisheng.com/cme/exam.jsp*
  11. // @match *://cme.haoyisheng.com/cme/examQuizFail.jsp*
  12. // @match *://bjsqypx.haoyisheng.com/qypx/bj/polyv.jsp*
  13. // @match *://bjsqypx.haoyisheng.com/qypx/bj/cc.jsp*
  14. // @match *://bjsqypx.haoyisheng.com/qypx/bj/exam.jsp*
  15. // @match *://bjsqypx.haoyisheng.com/qypx/bj/examQuizFail.jsp*
  16. // @match *://www.cmechina.net/cme/polyv.jsp*
  17. // @match *://www.cmechina.net/cme/study2.jsp*
  18. // @match *://www.cmechina.net/cme/exam.jsp*
  19. // @match *://www.cmechina.net/cme/examQuizFail.jsp*
  20. // @match *://hb.cmechina.net/cme/polyv.jsp*
  21. // @match *://hb.cmechina.net/cme/study2.jsp*
  22. // @match *://hb.cmechina.net/cme/exam.jsp*
  23. // @match *://hb.cmechina.net/cme/examQuizFail.jsp*
  24. // @license MIT
  25. // @icon https://raw.githubusercontent.com/houziyu/cme-haoyisheng-helper/main/favicon.ico
  26. // @run-at document-end
  27. // @grant unsafeWindow
  28. // @grant GM_addStyle
  29. // @grant GM_getValue
  30. // @grant GM_setValue
  31. // @grant GM_deleteValue
  32. // @grant GM_listValues
  33. // @grant GM_openInTab
  34. // @grant GM_notification
  35. // @grant GM_xmlhttpRequest
  36. // @grant GM_getResourceText
  37. // ==/UserScript==
  38. Object.defineProperty(unsafeWindow.document, 'querySelector', {
  39. value: unsafeWindow.document.querySelector,
  40. writable: false,
  41. });
  42.  
  43. (function () {
  44. 'use strict';
  45. /**
  46. * 动态添加样式
  47. * code from https://github.com/hmjz100/Online-disk-direct-link-download-assistant/
  48. */
  49. function addStyle(id, tag, css, element) {
  50. tag = tag || 'style';
  51. element = element || 'body';
  52. let doc = document, styleDom = doc.getElementById(id);
  53. if (styleDom) styleDom.remove();
  54. let style = doc.createElement(tag);
  55. style.rel = 'stylesheet';
  56. style.id = id;
  57. tag === 'style' ? style.innerHTML = css : style.href = css;
  58. doc.getElementsByTagName(element)[0].appendChild(style);
  59. }
  60. addStyle('swal-pub-style', 'style', '.swal2-container{z-index:1999;}' + GM_getResourceText('Swal'));
  61.  
  62. // /**
  63. // * 获取本地存储的正确答案对象
  64. // * @deprecated 由于题目顺序会变化, 现在已废弃, 不再存储答案
  65. // */
  66. // const getAnswerObject = () => {
  67. // const answerObject = localStorage.getItem('right_answer_obj') || '{}';
  68. // return JSON.parse(answerObject);
  69. // };
  70. const buttonCssText = 'position: absolute;z-index: 99;top: -50px;right: 0;padding:10px;cursor:pointer;background-color: #3087d9;color: #fff;box-shadow: 0px 0px 12px rgba(0, 0, 0, .12);';
  71. function getUrlParams(name) {
  72. const urlSearchParams = new URLSearchParams(window.location.search);
  73. return urlSearchParams.get(name);
  74. }
  75. function getLastUrlPath() {
  76. const pathList = window.location.pathname.split('/');
  77. return pathList[pathList.length - 1];
  78. }
  79. // 用来区分当前在哪个页面
  80. const lastPath = getLastUrlPath();
  81. // 得到试卷ID, 来定义广播频道名称
  82. const examId = `${getUrlParams('course_id')}_${getUrlParams('paper_id')}`;
  83.  
  84. /**
  85. * 创建广播通信频道
  86. * Generated By Microsoft Copilot
  87. */
  88. // Create a unique channel name
  89. const channelName = `NoMoreExam_${examId}`;
  90. // Send a message to the other tab
  91. function sendMessageToOtherTab(message) {
  92. const channel = new BroadcastChannel(channelName);
  93. channel.postMessage(message);
  94. }
  95. // Listen for messages from other tabs
  96. function setupMessageListener(handler) {
  97. const channel = new BroadcastChannel(channelName);
  98. channel.onmessage = (event) => {
  99. const receivedMessage = event.data;
  100. handler(receivedMessage);
  101. };
  102. }
  103.  
  104. // 获取单选题的下一个选项
  105. function getNextChoice(str, questionIndex) {
  106. const code = str.charCodeAt(0) + 1;
  107. if (code === 70) {
  108. alert(`全部遍历但未找到第${questionIndex + 1}题的正确答案, 请确定是使用脚本按钮开始答题! 请关闭此页面重新开始考试`);
  109. return 'A';
  110. }
  111. return String.fromCharCode(code);
  112. }
  113. // 循环组合以获取多选题下一个选项
  114. function getNextMultipleChoice(str, questionIndex) {
  115. const dic = ['ABCDE', 'BCDE', 'ACDE', 'ABDE', 'ABCE', 'ABCD', 'CDE', 'BDE', 'BCE', 'BCD', 'ADE', 'ACE', 'ACD', 'ABE', 'ABD', 'ABC', 'DE', 'CE', 'CD', 'BE', 'BD', 'BC', 'AE', 'AD', 'AC', 'AB', 'E', 'D', 'C', 'B', 'A'];
  116. const index = dic.indexOf(str);
  117. if (index === dic.length - 1) {
  118. alert(`全部遍历但未找到第${questionIndex + 1}题的正确答案, 请确定是使用脚本按钮开始答题! 请关闭此页面重新开始考试`);
  119. return dic[0];
  120. }
  121. return dic[index + 1];
  122. }
  123.  
  124. // 考试结果页面进行遍历, 得到正确答案
  125. if (lastPath === ('examQuizFail.jsp')) {
  126. // 北京全员培训
  127. if (location.host === 'bjsqypx.haoyisheng.com') {
  128. const error_order = getUrlParams('error_order');
  129. sendMessageToOtherTab(error_order);
  130. window.close();
  131. return;
  132. }
  133. // 获取当前选的答案
  134. const nowAnswerStr = window.location.search.split('ansList=')[1].split('&')[0]; // A,A,A,A,A
  135. const nowAnswerList = nowAnswerStr.split(',');
  136.  
  137. let currentQuestionIndex = 0;
  138. const answersList = document.querySelectorAll('.answer_list h3');
  139. let finished = true;
  140. for (let i = 0; i < answersList.length; i++) {
  141. currentQuestionIndex = i;
  142. if (answersList[i].className.includes('cuo')) {
  143. finished = false;
  144. if (nowAnswerList[i].length === 1) {
  145. nowAnswerList[i] = getNextChoice(nowAnswerList[i], currentQuestionIndex);
  146. } else {
  147. nowAnswerList[i] = getNextMultipleChoice(nowAnswerList[i], currentQuestionIndex);
  148. }
  149. window.location.href = window.location.href.replace(nowAnswerStr, nowAnswerList.join(','));
  150. break;
  151. }
  152. }
  153. if (finished) {
  154. sendMessageToOtherTab(JSON.stringify(nowAnswerList));
  155. window.close();
  156. }
  157. }
  158. // 考试页面填写初始答案和正确答案, 并提交
  159. if (lastPath === ('exam.jsp')) {
  160. // 北京全员培训
  161. const isBjsqypx = location.host === 'bjsqypx.haoyisheng.com';
  162. // 所有题目列表
  163. const questionsList = isBjsqypx ? document.querySelectorAll('.kaoshi dl') : document.querySelectorAll('.exam_list li');
  164. const submitBtn = isBjsqypx ? document.querySelector('.but_box .btn1') : document.querySelector('#tjkj');
  165. const nowAnswerObjList = [];
  166. // 遍历题目自动选择答案
  167. const autoSelectAnswer = answerArray => {
  168. const indexMap = {
  169. 'A': 0,
  170. 'B': 1,
  171. 'C': 2,
  172. 'D': 3,
  173. 'E': 4
  174. };
  175. for (let i = 0; i < questionsList.length; i++) {
  176. const answer = answerArray[i];
  177. const optionsList = questionsList[i].querySelectorAll('p');
  178. // 单选直接选择
  179. if (questionsList[i].querySelectorAll('input[type="radio"]').length > 0) {
  180. const index = indexMap[answer] || 0;
  181. const answerItem = optionsList[index];
  182. // 根据答案选项定位到勾选框
  183. const input = answerItem.children[0];
  184. nowAnswerObjList[i] = {
  185. type: 1,
  186. value: input.value
  187. };
  188. input.dispatchEvent(new MouseEvent('click'));
  189. continue;
  190. }
  191. for (let i = 0; i < optionsList.length; i++) {
  192. const answerItem = optionsList[i];
  193. nowAnswerObjList[i] = {
  194. type: 2,
  195. value: answer
  196. };
  197. const input = answerItem.children[0];
  198. if (answer.includes(input.value) && !input.checked) {
  199. input.dispatchEvent(new MouseEvent('click'));
  200. }
  201. }
  202. }
  203. };
  204. const messageHandler = message => {
  205. // 得到正确答案返回后, 直接填写并提交
  206. autoSelectAnswer(JSON.parse(message));
  207. // 最终答题时移除target=_blank属性
  208. document.querySelector('form').removeAttribute('target');
  209. submitBtn.dispatchEvent(new MouseEvent('click'));
  210. };
  211. const qypxMessageHandler = message => {
  212. const errorOrderList = message.split(',');
  213. errorOrderList.forEach(order => {
  214. const index = parseInt(order, 10) - 1;
  215. const answer = nowAnswerObjList[index].value;
  216. nowAnswerObjList[index].value = nowAnswerObjList[index].type === 1 ? getNextChoice(answer) : getNextMultipleChoice(answer);
  217. });
  218. autoSelectAnswer(nowAnswerObjList.map(item => item.value));
  219. document.querySelector('form').setAttribute('target', '_blank');
  220. submitBtn.dispatchEvent(new MouseEvent('click'));
  221. };
  222.  
  223. // 设置广播消息监听器
  224. setupMessageListener(isBjsqypx ? qypxMessageHandler : messageHandler);
  225.  
  226. // 控件
  227. const examSkipButton = document.createElement('button');
  228.  
  229. examSkipButton.innerText = '考试? 拿来吧你!';
  230. examSkipButton.id = 'exam_skip_btn';
  231. examSkipButton.style.cssText = buttonCssText;
  232. examSkipButton.style.top = '55px';
  233. examSkipButton.style.right = '150px';
  234.  
  235. examSkipButton.addEventListener('click', () => {
  236. // 多选全选, 单选选A
  237. const answersArray = new Array(questionsList.length).fill('ABCDE');
  238. autoSelectAnswer(answersArray);
  239. document.querySelector('form').setAttribute('target', '_blank');
  240. submitBtn.dispatchEvent(new MouseEvent('click'));
  241. });
  242.  
  243. if (isBjsqypx) {
  244. examSkipButton.style.top = '0px';
  245. examSkipButton.style.right = '50px';
  246. examSkipButton.style.border = 'none';
  247. document.querySelector('.content').appendChild(examSkipButton);
  248. } else {
  249. document.querySelector('.main').appendChild(examSkipButton);
  250. }
  251.  
  252. if (localStorage.getItem('script_auto_exam') === 'true') {
  253. examSkipButton.dispatchEvent(new MouseEvent('click'));
  254. }
  255. return;
  256. }
  257. // 视频跳过
  258. setTimeout(() => {
  259. let fuckingPlayer = null;
  260.  
  261. let timer = setInterval(() => {
  262. if (document.querySelector(".main") !== null) {
  263. document.querySelector('.main').style.marginTop = '40px';
  264. clearInterval(timer);
  265. }
  266. if (document.querySelector("video") !== null) {
  267. const video = document.querySelector('.pv-video') || document.querySelector('video');
  268. const parent = video.parentElement;
  269. const videoSkipButton = document.createElement('button');
  270. const selecterLabel = document.createElement('label');
  271. const playRateSelecter = document.createElement('select');
  272. const playRateCheckbox = document.createElement('input');
  273. const checkboxContainer = document.createElement('div');
  274. const videoCheckboxLabel = document.createElement('label');
  275. const videoCheckbox = document.createElement('input');
  276. const examCheckboxLabel = document.createElement('label');
  277. const examCheckbox = document.createElement('input');
  278. const containerCssText = 'position: absolute;height: 37px;line-height: 37px;top: -50px;right: 140px;';
  279. const labelCssText = 'vertical-align: middle;margin-right: 5px;line-height: 37px;color: #3087d9;font-size: 15px;';
  280. const controllerCssText = 'vertical-align: middle;cursor: pointer; margin-right: 5px;';
  281. checkboxContainer.style.cssText = containerCssText;
  282. // 跳过按钮
  283. videoSkipButton.innerText = '看视频? 拿来吧你!';
  284. videoSkipButton.style.cssText = buttonCssText;
  285. // 自动看完
  286. videoCheckboxLabel.innerText = '自动看完:';
  287. videoCheckboxLabel.style.cssText = labelCssText;
  288. videoCheckbox.type = 'checkbox';
  289. videoCheckbox.style.cssText = controllerCssText;
  290. // 自动开考
  291. examCheckboxLabel.innerText = '进入考试后自动开考:';
  292. examCheckboxLabel.style.cssText = labelCssText;
  293. examCheckbox.type = 'checkbox';
  294. examCheckbox.style.cssText = controllerCssText;
  295. // 倍速
  296. selecterLabel.innerText = '倍速:';
  297. selecterLabel.style.cssText = labelCssText;
  298. playRateSelecter.style.cssText = controllerCssText;
  299. playRateSelecter.style.border = '1px solid #000';
  300. playRateCheckbox.type = 'checkbox';
  301. playRateCheckbox.style.cssText = controllerCssText;
  302. // 倍速选择器初始化选项
  303. for (let i = 1; i <= 15; i++) {
  304. const option = document.createElement('option');
  305. option.value = i;
  306. option.label = i;
  307. playRateSelecter.appendChild(option);
  308. }
  309. playRateSelecter.addEventListener('change', () => {
  310. localStorage.setItem('play_back_rate', playRateSelecter.value);
  311. if (palyRateEnable) {
  312. video.playbackRate = parseInt(playRateSelecter.value);
  313. }
  314. });
  315. playRateCheckbox.addEventListener('change', e => {
  316. const value = e.target.checked;
  317. localStorage.setItem('play_back_rate_enable', JSON.stringify(value));
  318. if (value) {
  319. video.playbackRate = parseInt(playRateSelecter.value);
  320. } else {
  321. video.playbackRate = 1;
  322. }
  323. });
  324. videoCheckbox.addEventListener('change', e => {
  325. const autoValue = e.target.checked;
  326. localStorage.setItem('script_auto_skip', JSON.stringify(autoValue));
  327. });
  328. examCheckbox.addEventListener('change', e => {
  329. const autoValue = e.target.checked;
  330. localStorage.setItem('script_auto_exam', JSON.stringify(autoValue));
  331. });
  332. videoSkipButton.addEventListener('click', () => {
  333. if (fuckingPlayer) {
  334. fuckingPlayer.setVolume(0);
  335. fuckingPlayer.play();
  336. fuckingPlayer.jumpToTime(fuckingPlayer.getDuration() - 0.5);
  337. } else {
  338. video.volume = 0;
  339. video.playbackRate = parseInt(playRateSelecter.value);
  340. video.currentTime = video.duration;
  341. }
  342. });
  343. if (document.querySelector('.content .h5')) {
  344. document.querySelector('.content .h5').style.marginBottom = '50px';
  345. checkboxContainer.style.top = '-45px';
  346. videoSkipButton.style.top = '-45px';
  347. videoSkipButton.style.border = 'none';
  348. }
  349. if (document.querySelector('.ccH5playerBox')) {
  350. document.querySelector('.ccH5playerBox').style.overflow = 'visible';
  351. }
  352. checkboxContainer.append(examCheckboxLabel, examCheckbox, videoCheckboxLabel, videoCheckbox, selecterLabel, playRateCheckbox, playRateSelecter);
  353. parent.append(checkboxContainer, videoSkipButton);
  354. clearInterval(timer);
  355. }
  356. }, 1000);
  357.  
  358. // 开放右键限制
  359. document.oncontextmenu = null;
  360. document.body.oncontextmenu = null;
  361. const elements = document.querySelectorAll('*');
  362. elements.forEach(el => el.oncontextmenu = null);
  363.  
  364. //绕过 F12 禁用
  365. document.onkeydown = null;
  366. document.body.onkeydown = null;
  367. function initPlayer() {
  368. const localNoticeSkip = localStorage.getItem('swal_notice_skip');
  369.  
  370. if (unsafeWindow.player && unsafeWindow.player.params) {
  371. unsafeWindow.player.params.rate_allow_change = true;
  372. fuckingPlayer = unsafeWindow.player;
  373. } else if (unsafeWindow.cc_js_Player && unsafeWindow.cc_js_Player.params) {
  374. unsafeWindow.cc_js_Player.params.rate_allow_change = true;
  375. fuckingPlayer = unsafeWindow.cc_js_Player;
  376. }
  377.  
  378. if (fuckingPlayer) {
  379. !localNoticeSkip && Swal.fire({
  380. title: "播放器获取成功",
  381. text: "倍速与一键看完功能已正常!",
  382. icon: "success"
  383. });
  384. localStorage.setItem('swal_notice_skip', 'true');
  385. } else {
  386. localStorage.removeItem('swal_notice_skip');
  387. Swal.fire({
  388. title: "播放器获取失败",
  389. text: "似乎网站未被正确兼容? 功能可能不正常",
  390. icon: "question"
  391. });
  392. }
  393. }
  394.  
  395. if (document.querySelector('.main')) {
  396. document.querySelector('.main').style.marginTop = '40px';
  397. }
  398. // 仅适用chromium
  399. unsafeWindow.clearInterval(1);
  400. initPlayer();
  401.  
  402. /* -------------- 根据本地存储, 对各项值预处理 -------------- */
  403. if (localStorage.getItem('script_auto_skip') === 'true') {
  404. videoCheckbox.checked = true;
  405. videoSkipButton.dispatchEvent(new MouseEvent('click'));
  406. }
  407. if (localStorage.getItem('script_auto_exam') === 'true') {
  408. examCheckbox.checked = true;
  409. }
  410.  
  411. const localRate = localStorage.getItem('play_back_rate');
  412. const palyRateEnable = localStorage.getItem('play_back_rate_enable');
  413. if (!localRate) {
  414. return;
  415. }
  416. let rate = parseInt(localRate);
  417. if (!isNaN(rate) && rate >= 1 && rate <= 15) {
  418. playRateSelecter.value = localRate;
  419. } else {
  420. playRateSelecter.value = '10';
  421. rate = 10;
  422. }
  423.  
  424. if (palyRateEnable === 'true') {
  425. playRateCheckbox.checked = true;
  426. video.playbackRate = rate;
  427. }
  428. }, 1500);
  429. })();