Greasy Fork is available in English.

tame QTKJ

at the end of with it.

Fra 02.02.2020. Se den seneste versjonen.

// ==UserScript==
// @name          tame QTKJ
// @namespace     Vionlentmonkey
// @version       3.1.3
// @description   at the end of with it.
// @author        someone
// @icon          http://www.moj.gov.cn/favicon.ico

// @match         http://218-94-1-181.sft.ipv6.jiangsu.gov.cn:8087/sfxzwsxy/*
// @match         http://218-94-1-179.sft.ipv6.jiangsu.gov.cn:8087/sfxzwsxy/*
// @match         http://218-94-1-175.sft.ipv6.jiangsu.gov.cn:8087/sfxzwsxy/*

// @match         http://218.94.1.181:8087/sfxzwsxy/*
// @match         http://218.94.1.179:8087/sfxzwsxy/*
// @match         http://218.94.1.175:8087/sfxzwsxy/*

// @match         http://218.94.1.181:5088/unzipapp/project/ware/attach/*

// @require       https://openuserjs.org/src/libs/sizzle/GM_config.js
// @require       https://cdn.jsdelivr.net/npm/vm.shortcut

// @require       https://greasyfork.org/scripts/381401-addstyle/code/addStyle.js
// @require       https://greasyfork.org/scripts/381403-fakenavigators/code/fakeNavigators.js

// @require       https://greasyfork.org/scripts/395748-wsxy-get-data/code/wsxy_get_data.js
// @require       https://greasyfork.org/scripts/395937-wsxy-autoserverselect/code/wsxy_autoServerSelect.js
// @require       https://greasyfork.org/scripts/395952-wsxy-autoloin/code/wsxy_autoLoin.js

// @grant         GM_getValue
// @grant         GM_setValue
// @grant         GM_registerMenuCommand
// @grant         GM_openInTab
// @grant         GM_notification
// @grant         GM_xmlhttpRequest
// @run-at        document-start

// ==/UserScript==

const body = document.body || document.documentElement;

const windowCSS = `
#Cfg {
  height: auto;
  background-color: lightblue;
}
#Cfg .reset_holder {
  float: left;
  position: relative;
  bottom: -1em;
}
#Cfg .saveclose_buttons {
  margin: 1em;
}
`;

const openCfg = () => {
  // 避免在包含框架的页面打开造成多个设置界面重叠
  if (window.top === window.self) {
    GM_config.open();
  }
};

GM_registerMenuCommand('江苏省司法行政网上学院个性化设置', openCfg);

GM_config.init({
  id: 'Cfg',
  title: '江苏省司法行政网上学院个性化设置',
  fields: {
    loginName: {
      section: ['登录', '完整填写尝试自动登录'],
      label: '账号',
      labelPos: 'right',
      type: 'text',
      default: ''
    },
    pwd: {
      label: '密码',
      labelPos: 'right',
      type: 'password',
      default: ''
    },
    reload: {
      label: '是否无限尝试',
      labelPos: 'right',
      type: 'checkbox',
      default: false
    },
    open_unclose: {
      section: ['操作', '后台批量打开新标签页'],
      label: '快捷键',
      labelPos: 'right',
      type: 'text',
      default: 'F8'
    },
    batch: {
      label: '批量打开个数',
      labelPos: 'right',
      type: 'int',
      default: 10
    },
    muted: {
      label: '静音播放',
      labelPos: 'right',
      type: 'checkbox',
      default: true
    }
  },
  css: windowCSS,
  events: {
    save: () => {
      GM_config.close();
      // 服务器选择页面或登录页面自动刷新
      if (
        location.pathname.match(/\/sfxzwsxy\/?(serverSelect.jsp)?#?$/i) ||
        location.pathname.startsWith('/sfxzwsxy/index.jsp')
      ) {
        location.reload(true);
      }
    }
  }
});

// 彩蛋:通关密语
const passPhrase = btoa(GM_config.get('pwd')).startsWith('TzUxbzE5');

// 从 QQ 等打开地址后会被加上奇葩后缀 http://218.94.1.179:8087/sfxzwsxy/#?tdsourcetag=s_pctim_aiomsg
const clearURLs = () => {
  const tracks = [
    // QQ
    '?tdsourcetag=s_pctim_aiomsg',
    // Weixin
    '?from=groupmessage',
    '&from=groupmessage',
    '?from=singlemessage',
    '&from=singlemessage',
    '?from=timeline',
    '&from=timeline',
    '/type/WeixinReadCount'
  ];
  for (let track of tracks) {
    if (location.href.endsWith(track)) {
      location.href = location.href.replace(track, '');
    }
  }
};
clearURLs();

// 统一域名
if (location.host.endsWith('.sft.ipv6.jiangsu.gov.cn:8087')) {
  location.host = location.host.replace('.sft.ipv6.jiangsu.gov.cn', '').replace('-', '.');
}

// http://218.94.1.175:8087/sfxzwsxy/
if (location.pathname.match(/\/sfxzwsxy\/?(serverSelect.jsp)?#?$/i)) {
  // 自动选择最空闲的服务器
  setInterval(autoServerSelect, 1000);
}

// 去除登陆验证码校验
// 曾使用 OCR 识别法,参考了 https://www.cnblogs.com/ziyunfei/archive/2012/10/05/2710349.html 但准确度有限。
if (location.pathname.startsWith('/sfxzwsxy/index.jsp')) {
  window.onload = () => {
    autoLogin();
  };
}

// 首页
if (
  location.href.endsWith('/sfxzwsxy/jypxks/index.html') ||
  location.href.endsWith('/sfxzwsxy/jypxks/index.html#')
) {
  // 修复页面过长无法完整显示的 Bug; 隐藏 密码修改提示 和 每日一题
  const css = `
  html[style="overflow: hidden;"] {
    overflow: visible !important;
  }
  #layui-layer1, #layui-layer-shade1, #layui-layer2, #layui-layer-shade2 {
    display: none !important;
  }
  `;
  addStyle(css);

  // 判断是否已完成任务。不等待页面载入完成就执行可能导致数据尚未成功获取而出错。
  window.onload = async () => {
    let userDataObj = await getUserData();
    let total_hour = userDataObj.total_hour; //规定需达到的总学时
    let required_hour = userDataObj.required_hour; //规定需达到的必修学时
    let required_credit = userDataObj.required_credit; //规定需达到的总学分
    let user_total_hour = userDataObj.user_total_hour || 0; //用户已获得的总学时
    let user_required_hour = userDataObj.user_required_hour || 0; //用户已获得的必修学时
    let user_required_credit = userDataObj.user_required_credit || 0; //用户已获得的总学分
    if (
      user_total_hour < total_hour ||
      user_required_hour < required_hour ||
      user_required_credit < required_credit
    ) {
      console.log(`本学年任务尚未完成:`);
      if (passPhrase) {
        GM_openInTab(`${location.origin}/sfxzwsxy/jypxks/modules/homepage/homepage.jsp`, false);
      } else {
        // 长时间不动会被弹出,故 30 分钟刷新一次
        setInterval(() => {
          location.reload(true);
        }, 1800000);
      }
      if (user_total_hour < total_hour) {
        console.log(`总学时差:${total_hour - user_total_hour}`);
      }
      if (user_required_hour < required_hour) {
        console.log(`必修学时差:${required_hour - user_required_hour}`);
      }
      if (user_required_credit < required_credit) {
        console.log(`总学分差:${required_credit - user_required_credit}`);
      }
    } else {
      console.log(`本学年任务已经完成`);
    }
  };
}
// 自动聚焦以便快捷键默认生效
let document_observer = new MutationObserver(() => {
  if (document.getElementById('mainFrame')) {
    document.getElementById('mainFrame').focus();
    console.log('focus');
  }
});
document_observer.observe(body, {
  childList: true,
  subtree: true
});

// 首页培训课程 iframe
if (location.href.endsWith('/sfxzwsxy/jypxks/modules/homepage/homepage.jsp')) {
  // F8 批量后台新标签页打开已报名课程播放页面
  VM.registerShortcut(GM_config.get('open_unclose'), () => {
    let i = 0;
    const trains = document.getElementsByClassName('course');
    for (let train of trains) {
      // 报名后不再隐藏
      let applyPk = train.getElementsByClassName('applyPk')[0].textContent;
      if (applyPk !== '') {
        let trainURL =
          location.origin +
          '/sfxzwsxy/jypxks/modules/train/ware/course_ware_view.jsp?applyPk=' +
          applyPk +
          '&courseType=1';
        GM_openInTab(trainURL, true);
        i++;
        // 疑似超过 6 个课程同时学习后台不计数
        if (i >= GM_config.get('batch')) {
          console.log(`已批量打开${i}个课程`);
          return;
        }
      }
    }
  });

  const auto3Credit = async () => {
    // 彩蛋:需要 iframe 提升才会执行
    if (!passPhrase && window.top === window.self) {
      let courses = document.querySelectorAll('#requiredCourseTable .course');
      for await (let c of courses) {
        let coursePk = c.getElementsByClassName('coursePk')[0].textContent;
        let title = c.getElementsByClassName('title')[0].getAttribute('title');
        let jdjs = c.getElementsByClassName('jdjs')[0].textContent; // 进度
        let csInfo = await getCourseInfo(coursePk);
        let csCredit = csInfo.courseCredit;
        // 彩蛋:需要 iframe 提升才会执行
        if (window.top === window.self) {
          if (jdjs === '未报名') {
            // 自动报名高学分课程
            if (csCredit > 1) {
              console.log(`${csCredit}学分:${title}`);
              c.click();
              console.log('click:' + title);
              const info = document.getElementsByClassName('layui-layer-btn0');
              if (info.length === 1) {
                document.getElementsByClassName('layui-layer-btn0')[0].click();
                console.log('报名:' + title);
              }
            }
          }
        }
      }
    }
  };

  const autoLearn = async () => {
    if (passPhrase && window.top === window.self) {
      // 30 分钟刷新一次
      setInterval(() => {
        location.reload(true);
      }, 1800000);
      /**
       * ✅:获取学时/学分,已报名课程学时/学分
       * ✅:若学分未满而有待考试课程则打开考试页面
       * 若已获得学时+已报名课程总学时未满则继续报名,哪项没满就报该项最大的
       * ✅:若已报名完成,满足考试条件,则完成现有考试并刷新
       * 若已报名足够学时和学分则开始听课
       */
      // 用户得分状态
      let userDataObj = await getUserData();
      //规定需达到的总学时
      let total_hour = userDataObj.total_hour;
      //规定需达到的必修学时
      let required_hour = userDataObj.required_hour;
      //规定需达到的总学分
      let required_credit = userDataObj.required_credit;
      //用户已获得的总学时
      let user_total_hour = userDataObj.user_total_hour || 0;
      //用户已获得的必修学时
      let user_required_hour = userDataObj.user_required_hour || 0;
      //用户已获得的总学分
      let user_required_credit = userDataObj.user_required_credit || 0;
      /*
      const csDataObj = await getCourseData();
      const required_courses = csDataObj.required_courses;
      */
      // 全部未完成必修课程。为同时解决学时学分问题,只关注必修课。
      const courses = document.querySelectorAll('#requiredCourseTable .course');
      // 初始化预期学分/学时为已得值,后续添加已报名课程数据
      let pendingCredit = user_required_credit;
      let pendingTime = user_total_hour;
      for await (let c of courses) {
        let coursePk = c.getElementsByClassName('coursePk')[0].textContent;
        /*
        let applyPk = '';
        for await (let r of required_courses) {
          if (r.course_pk === coursePk) {
            applyPk = r.apply_pk;
            console.log(applyPk);
          }
        }
        */
        let jdjs = c.getElementsByClassName('jdjs')[0].textContent; // 完成进度定性
        if (jdjs === '完成进度') {
          let csInfo = await getCourseInfo(coursePk);
          let csCredit = csInfo.courseCredit;
          let csTime = csInfo.courseTime;
          pendingCredit += csCredit;
          pendingTime += csTime;
        }
      }
      console.log(`已获得:必修学时:${user_required_hour},总学分${user_required_credit}`);
      // 判断是否已完成。首次在新标签页打开本页显然是未完成,但刷新后可能进入已完成状态。
      if (
        user_total_hour >= total_hour &&
        user_required_hour >= required_hour &&
        user_required_credit >= required_credit
      ) {
        console.log(`本学年任务已经完成`);
        return;
      } else if (pendingTime < total_hour) {
        console.log(`已报名课程可得学时:${pendingTime},学分:${pendingCredit},将继续报名。`);
        // 数组存储所有未报名课程故的课时数据
        let timesArray = [];
        // 获取所有未报名课程的子节点。若直接使用所有必修课程,可能出现课程数字相同而对已报名课程二次点击。
        let noneCoursesJdpoint = document.querySelectorAll(
          '#requiredCourseTable .course > .jdpoint[style]'
        );
        for await (let n of noneCoursesJdpoint) {
          // 由子节点获取父节点从而获取课程编号
          let coursePk = n.parentNode.getElementsByClassName('coursePk')[0].textContent;
          let csInfo = await getCourseInfo(coursePk);
          let csTime = csInfo.courseTime;
          timesArray.push(csTime);
        }
        let longest = Math.max(...timesArray);
        console.log(longest);
        let indexMax = timesArray.indexOf(longest);
        // 出现过该错误 indexMax === -Infinity 导致 TypeError: noneCoursesJdpoint[timesArray.indexOf(...)] is undefined
        if (noneCoursesJdpoint[indexMax]) {
          noneCoursesJdpoint[indexMax].parentNode.click();
        } else {
          location.reload(true);
        }
        const info = document.getElementsByClassName('layui-layer-btn0');
        if (info.length === 1) {
          document.getElementsByClassName('layui-layer-btn0')[0].click();
        }
      } else if (
        pendingTime >= total_hour &&
        pendingCredit < required_credit &&
        // 临时处理,避免出现学分不够的误判
        document.querySelectorAll('div[id^="courseExam"] > a[title]').length === 0
      ) {
        // 因为全部学习必修课,出现本状况可能很小,暂不处理
        console.log(
          `已报名课程可得学时:${pendingTime},学分:${pendingCredit},预期学时已满但预期学分不够,报名最大学分课程。`
        );
      } else if (
        pendingTime >= total_hour
        // 临时处理,避免出现学分不够的误判
        // && pendingCredit >= required_credit
      ) {
        console.log('报名已完成预期');
        if (user_required_hour < total_hour) {
          console.log('学时未满,自动打开已报名课程,将定时关闭');
          const skipLearn = async (applyPk, isNewPlayer) => {
            if (!isNewPlayer && i < GM_config.get('batch')) {
              let trainURL =
                location.origin +
                '/sfxzwsxy/jypxks/modules/train/ware/course_ware_view.jsp?applyPk=' +
                applyPk +
                '&courseType=1';
              const openClose = GM_openInTab(trainURL, true);
              i++;
              // 25 分钟自动关闭
              setTimeout(openClose.close, 1500000);
            }
            // 疑似超过 6 个课程同时学习后台不计数
            else if (i >= GM_config.get('batch')) {
              console.log(`已批量打开${i}个课程`);
              return;
            }
          };
          let i = 0;
          for await (let c of courses) {
            let applyPk = c.getElementsByClassName('applyPk')[0].textContent;
            // 部分少见的新型播放器会弹出 confirm,避开为宜。
            // 未报名课程 applyPk === '',故不适合使用 for length++ 循环
            skipNewVideoPlayerType(applyPk, skipLearn);
          }
        } else if (
          user_required_credit < required_credit &&
          document.querySelectorAll('div[id^="courseExam"] > a[title]').length > 0
        ) {
          console.log('学时已满,学分未满,有待考试课程');
          const exams = document.querySelectorAll('div[id^=courseExam] > a[title]');
          for await (let exam of exams) {
            const examURL =
              location.origin + '/sfxzwsxy/' + exam.getAttribute('onclick').split("'")[1];
            // 魔法打开的考卷确认交卷后不能自动关闭,只得如此暴力关闭
            let autoExam = GM_openInTab(examURL, true);
            setTimeout(autoExam.close, 1200000);
          }
        }
      }
    }
  };

  // 考试提醒 - 课程考试 - 修改为在新标签页打开
  const openExam = async () => {
    // 第一页 #courseExam1,第二页 #courseExam2
    const exams = document.querySelectorAll('div[id^=courseExam] > a[title]');
    for await (let exam of exams) {
      const examURL = location.origin + '/sfxzwsxy/' + exam.getAttribute('onclick').split("'")[1];
      exam.href = examURL;
      exam.onclick = '';
      exam.target = '_blank';
    }
  };

  // 考试提醒 - 课程考试 - 在新标签页打开对应答案
  const addAnswer = async () => {
    // 因网络获取与解析需要时间,与 openExam() 整合会造成继发延迟。
    const csDataObj = await getCourseData();
    const exam_courses = csDataObj.exam_courses;
    // 第一页 #courseExam1,第二页 #courseExam2
    const exams = document.querySelectorAll('div[id^=courseExam] > a[title]');
    for await (let exam of exams) {
      const title = exam.getAttribute('title');
      for await (let e of exam_courses) {
        let course_pk = '';
        let answerURL = '';
        if (e.course_name === title) {
          course_pk = e.course_pk;
          answerURL =
            location.origin +
            '/sfxzwsxy//jypxks/modules/train/course/subject_list.jsp?coursePk=' +
            course_pk +
            '&op=view';
          exam.addEventListener('click', () => {
            GM_openInTab(answerURL, true);
            GM_notification('答案已同步在隔壁标签页打开。\n暂请考试结束后手动关闭。');
          });
        }
      }
    }
  };

  // 最新知识库
  const openKnowledge = () => {
    const knowledges = document.querySelectorAll('#knowledgeType a[title][href="#"]');
    for (let knowledge of knowledges) {
      const kURL =
        location.origin +
        '/sfxzwsxy/jypxks/modules/learn/document/learn/document_learn_text.jsp?fkNodePk=' +
        knowledge
          .getAttribute('onclick')
          .split('(')[1]
          .split(')')[0];
      //console.log(kURL);
      knowledge.href = kURL;
      knowledge.onclick = '';
      knowledge.target = '_blank';
    }
  };

  // 并发执行
  window.onload = async () => {
    let funs = [addAnswer(), auto3Credit(), autoLearn(), openExam(), openKnowledge()];
    for await (let f of funs) {
      await f;
    }
  };
}

// 培训课程查询 iframe
if (location.href.endsWith('/sfxzwsxy/jypxks/modules/train/query/course_query.jsp')) {
  // 清理“参加培训”和“查看”链接
  const reallinks = () => {
    let trains = document.querySelectorAll('#trainCourseList a[onclick^=bindBeginTrainEvents]');
    for (let train of trains) {
      let applyPk = train.getAttribute('onclick').split('"')[1];
      let trainURL =
        location.origin +
        '/sfxzwsxy/jypxks/modules/train/ware/course_ware_view.jsp?applyPk=' +
        applyPk +
        '&courseType=1';
      train.href = trainURL;
      train.onclick = '';
      train.target = '_blank';
    }
    let answers = document.querySelectorAll(
      '#trainCourseList a[onclick^=bindViewCourseInfoEvents]'
    );
    for (let answer of answers) {
      let coursePk = answer.getAttribute('onclick').split('"')[1];
      let answerURL =
        location.origin +
        '/sfxzwsxy/jypxks/modules/train/course/course_view.jsp?coursePk=' +
        coursePk;
      answer.href = answerURL;
      answer.onclick = '';
      answer.target = '_blank';
    }
  };
  // 保证翻页生效
  let document_observer = new MutationObserver(() => {
    reallinks();
  });
  document_observer.observe(body, {
    childList: true,
    subtree: true
  });

  // F8 批量后台新标签页打开课程播放页面
  VM.registerShortcut(GM_config.get('open_unclose'), () => {
    const trains = document.querySelectorAll('#trainCourseList a[onclick^=bindBeginTrainEvents]');
    // 疑似超过 6 个课程同时学习后台不计数
    for (let i = 0; i < GM_config.get('batch'); i++) {
      let applyPk = trains[i].getAttribute('onclick').split('"')[1];
      let trainURL =
        location.origin +
        '/sfxzwsxy/jypxks/modules/train/ware/course_ware_view.jsp?applyPk=' +
        applyPk +
        '&courseType=1';
      GM_openInTab(trainURL, true);
      if (i >= GM_config.get('batch') - 1) {
        console.log(`已批量打开${i + 1}个课程`);
        return;
      }
    }
  });
}

// 培训课程查询 - 查看 - 题干 iframe
if (
  // 点击 “下一页” 或 “上一页” 后 iframe 实际地址会去除 .jsp 之后的尾巴
  location.pathname.startsWith('/sfxzwsxy//jypxks/modules/train/course/subject_list.jsp')
) {
  // 清理“题干”链接
  window.onload = () => {
    const subjects = document.querySelectorAll('a[onclick^=viewSubject]');
    for (let subject of subjects) {
      const subjectPk = subject
        .getAttribute('onclick')
        .split('(')[1]
        .split(')')[0];
      const subjectURL =
        location.origin +
        '/sfxzwsxy//jypxks/modules/train/course/subject_view.jsp?subjectPk=' +
        subjectPk;
      subject.href = subjectURL;
      subject.onclick = '';
      subject.target = '_blank';
    }
  };
}

// 课程视频播放 跨域 iframe
if (location.href.startsWith('http://218.94.1.181:5088/unzipapp/project/ware/attach/')) {
  // 旧播放器在 Windows 下要求 Flash,新播放器不兼容苹果系列。
  fakeUA('Linux');
  const isFlash = () => {
    const html5Player = document.getElementById('course_player5'); // 旧播放器
    const video_media = document.getElementById('video_media'); // 新播放器
    if (video_media) {
      // 新播放器用 confirm 询问是否继续学习,阻塞脚本运行,可能跳过这类课程比较好。
      // 通过 iframe 源代码可以明显看到不同
      // 测试例:http://218.94.1.175:8087/sfxzwsxy/jypxks/modules/train/ware/course_ware_view.jsp?applyPk=3063755&courseType=1
      console.log('新播放器');
      if (GM_config.get('muted')) {
        video_media.querySelector('video').muted = true;
        // https://developer.mozilla.org/docs/Web/Guide/Events/Media_events
        video_media.querySelector('video').play();
        //.next(clearInterval(noFlash));
      }
    } else if (html5Player) {
      console.log('旧播放器');
      // 静音模式下自动播放无需用户授权
      // https://developer.mozilla.org/docs/Web/Media/Autoplay_guide#Autoplay_availability
      if (GM_config.get('muted')) {
        html5Player.muted = true;
        // https://developer.mozilla.org/docs/Web/Guide/Events/Media_events
        html5Player.play().next(clearInterval(noFlash));
      }
    } else {
      // 旧版本播放器能否成功调用 HTML5 似乎是玄学问题,检测不到 HTML5 播放器则刷新
      location.reload(true);
    }
  };
  const noFlash = setInterval(isFlash, 1000);

  const learn = () => {
    // 自动从课程封面进入播放页面
    if (document.querySelector('img[src="courseware/iconImg/z3.png"]')) {
      document.querySelector('img[src="courseware/iconImg/z3.png"]').click();
    }
    // 老播放器用的 confirm 对话框手动点击即暂停,新版的 alert 无法模拟点击但手动点击不影响后台播放
    if (document.getElementById('cancel')) {
      //document.getElementById('confirm').click(); // 继续学习;可能需要多次重复才能完成一课时。
      document.getElementById('cancel').click(); // 大侠还请重新来过
    }
  };

  const option = () => {
    // 旧播放器自动做题,找不到测试例进一步优化。
    if (document.querySelectorAll('div.option>label>input[name="que"]').length > 0) {
      document.querySelectorAll('div.option>label>input[name="que"]')[0].click();
      document.querySelectorAll('div.option>label>input[name="que"]')[1].click(); // 兼容多选题
      document.getElementsByClassName('button')[0].click(); // 提交
      if (document.getElementsByClassName('button_xia').length === 1) {
        document.getElementsByClassName('button_xia')[0].click(); // 下一题
      }
      if (document.getElementsByClassName('button_wan').length === 1) {
        document.getElementsByClassName('button_wan')[0].click(); // 完成
      }
      // .click() 有反向操作风险
      document.getElementById('course_player5').play();
    }
  };
  let document_observer = new MutationObserver(() => {
    learn();
    option();
  });
  document_observer.observe(body, {
    attributes: true,
    subtree: true
  });
}

// 在线考试 - 课程考试 iframe
if (
  location.pathname.startsWith(
    '/sfxzwsxy/jypxks/modules/examination/course_examine/course_exam_list.jsp'
  )
) {
  // 清理“参加考试”链接,新标签页打开考试及答案。
  window.onload = () => {
    const exams = document.querySelectorAll('a[href="#"][onclick^=openWindowFullScreen]');
    for (let exam of exams) {
      const examURL = location.origin + '/sfxzwsxy/' + exam.getAttribute('onclick').split("'")[1];
      const coursePk = examURL.split('course_pk=')[1].split('&')[0];
      const answerURL =
        location.origin +
        '/sfxzwsxy//jypxks/modules/train/course/subject_list.jsp?coursePk=' +
        coursePk +
        '&op=view';
      exam.href = examURL;
      exam.onclick = '';
      exam.target = '_blank';
      exam.addEventListener('click', () => {
        GM_openInTab(answerURL, true);
        GM_notification('答案已同步在隔壁标签页打开。\n暂请考试结束后手动关闭。');
      });
    }
  };
}

// 考试
if (
  location.pathname ===
  '/sfxzwsxy/jypxks/modules/examination/course_examine/course_examine_test.jsp'
) {
  window.onload = async () => {
    if (passPhrase) {
      // 本考试所有试题
      let topics = document.getElementsByClassName('topic-tms');
      for await (let topic of topics) {
        // 题号
        let pkid = topic.querySelector('a[pkid]').getAttribute('pkid');
        // 本题答案
        let subjectDataMap = await getSubjectData(pkid);
        // 本题选项
        let options = topic.querySelectorAll('.tms-Right-wrong > p > a');
        for (let option of options) {
          let optionText = option.textContent.trim();
          if (subjectDataMap.get('questionType') === '判断题') {
            if (option.textContent.trim() === subjectDataMap.get('judgementAnswer')) {
              option.click();
            }
          } else {
            // 选择题选项内容带着序号与空格,如“A ”,故获取第三个开始的子串
            if (subjectDataMap.get(optionText.substring(2)) === '是') {
              option.click();
            }
          }
        }
      }
      // 交卷
      if (document.getElementsByClassName('subline _submit').length === 1) {
        document.getElementsByClassName('subline _submit')[0].click();
      }
      // 确认
      if (document.getElementsByClassName('layui-layer-btn0').length === 1) {
        document.getElementsByClassName('layui-layer-btn0')[0].click();
      }
    }
  };
}