Greasy Fork

智慧树/知到网课小助手[修复BUG]-[支持考试]-[最全题库]

智慧树网课助手,支持很多功能,[修复BUG]-[支持考试]-[最全题库]

目前为 2021-12-13 提交的版本。查看 最新版本

// ==UserScript==
// @name         智慧树/知到网课小助手[修复BUG]-[支持考试]-[最全题库]
// @namespace    shushoujiu
// @version      1.0.0
// @description  智慧树网课助手,支持很多功能,[修复BUG]-[支持考试]-[最全题库]
// @author       shushoujiu
// @match        *://*.zhihuishu.com/*
// @connect      cx.icodef.com
// @run-at       document-end
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_setClipboard
// @grant        GM_setValue
// @resource css https://unpkg.com/[email protected]/dist/css/bootstrap.min.css
// @license      MIT
// @original-script https://greasyfork.org/scripts/380506
// @original-author wyn665817
// @original-license MIT
// ==/UserScript==

// 设置修改后,需要刷新或重新打开网课页面才会生效
var setting = {
    // 5E3 == 5000,科学记数法,表示毫秒数
    time: 5e3, // 默认响应速度为5秒,不建议小于3秒

    // 1代表开启,0代表关闭
    video: 0, // 视频支持课程、见面课,默认关闭
    work: 1, // 自动答题功能,支持章测试、考试,高准确率,默认开启
    jump: 0, // 自动切换视频,支持课程、见面课,默认关闭
    habit: "25", // 限制共享课视频挂机时长,单位是分钟,如需挂机习惯分,可以修改参数为'30',默认不限制

    // 仅开启video时,修改此处才会生效
    line: "流畅", // 视频播放的默认线路,可选参数:['高清', '流畅', '校内'],默认'流畅'
    vol: "10", // 默认音量的百分数,设定范围:[0,100],'0'为静音,默认'0'
    speed: "1", // 进度统计速率,高倍率可以快速完成任务点,设定范围:(0,+∞),默认'1.5'倍
    // 上方参数支持在页面改动,下方参数仅支持代码处修改
    que: 0, // 屏蔽视频时间点对应的节试题,取消屏蔽则自动切换为模拟点击关闭弹题,默认关闭
    danmu: 0, // 见面课弹幕,关闭后在网页中无法手动开启,默认关闭

    // 仅开启work时,修改此处才会生效
    none: 0, // 无匹配答案时执行默认操作,默认关闭
    hide: 0, // 不加载答案搜索提示框,键盘↑和↓可以临时移除和加载,默认关闭
    api: [{ url: "http://cx.icodef.com/wyn-nb?v=3", method: "POST" }],
  },
  _self = unsafeWindow,
  url = location.pathname,
  $ = _self.jQuery,
  vjsC = _self.vjsComponent;
GM_addStyle(GM_getResourceText("css"));

String.prototype.toCDB = function () {
  return this.replace(/\s/g, "")
    .replace(/[\uff01-\uff5e]/g, function (str) {
      return String.fromCharCode(str.charCodeAt(0) - 65248);
    })
    .replace(/[“”]/g, '"')
    .replace(/[‘’]/g, "'")
    .replace(/。/g, ".");
};

// setting.time += Math.ceil(setting.time * Math.random()) - setting.time / 2;
setting.queue = setting.curs = [];

if (!$) {
} else if (url == "/live/vod_room.html") {
  courseFn(0);
} else if (url == "/videoStudy.html") {
  var xhr = _self.XMLHttpRequest.prototype;
  courseFn(1, xhr.open, setting.habit * 6e4);
} else if (url == "/portals_h5/2clearning.html") {
  courseFn(2);
} else if (url.match("/sourceLearning")) {
  courseFn(3);
} else if (location.hostname.match("examh5")) {
  setTimeout(relieveLimit, 100, document);
  if (location.hash.match(/dohomework|doexamination/) && setting.work)
    beforeFind();
  $(window).on("hashchange", function () {
    setting.work && location.reload();
  });
} else if (url == "/shareCourse/questionDetailPage") {
  setTimeout(relieveLimit, 100, document);
  $("textarea[oncut]").each(function () {
    setTimeout(relieveLimit, 100, this);
  });
}

function courseFn(tip, open, habit) {
  setting.jump && setInterval(checkToNext, setting.time);
  habit && setTimeout(totalTime, setting.time, habit);
  if (!setting.video) return;
  // _self.PlayerUtil.debugMode = true;
  setting.tip = tip;
  _self.vjsComponent = vjsComponent;
  $(document)
    .on("click", ".definiLines b", function () {
      setting.line = { xiaonei: "校内", line1gq: "高清", line1bq: "流畅" }[
        this.classList[0]
      ];
    })
    .on("mouseup click", function () {
      if (!_self.PlayerStarter.playerArray[0]) return;
      setting.vol =
        _self.PlayerStarter.playerArray[0].player.cache_.volume * 100;
    })
    .on("click", ".speedList div", function () {
      setting.speed = $(this).attr("rate");
    });
  if (setting.tip != 1) return;
  setting.que
    ? (xhr.open = function (type, url, async) {
        if (url.match("/loadVideoPointerInfo")) type = "GET";
        return open.call(this, type, url, async);
      })
    : setInterval(doTest, 1e3);
}

function vjsComponent(obj) {
  var options = obj.options,
    line = $.map(options.sourceSrc.lines, function (value) {
      return value.lineName.replace("标准", "高清");
    }),
    vol = setting.vol > 100 ? 100 : Math.round(setting.vol);
  options.volume = vol > 0 ? vol / 100 : 0;
  options.autostart = true;
  setting.speed = setting.speed > 0 ? +setting.speed : 1;
  options.rate =
    $.inArray(setting.speed, [1, 1.25, 1.5]) < 0 ? options.rate : setting.speed;
  setting.tip && obj.callback.playbackRate(setting.speed);
  options.chooseLine =
    $.inArray(setting.line, line) + 1 || options.chooseLine + 1;
  options.src =
    options.sourceSrc.lines[--options.chooseLine].lineUrl || options.src;
  if (!setting.danmu) {
    obj.defOptions.control.danmuBtn = false;
    delete options.control.danmuBtn;
  }
  obj.player.on("loadstart", function () {
    this.loop(true);
    this.play();
    $(".speedBox span").text("X " + setting.speed);
  });
  vjsC.call(this, obj);
}

function totalTime(habit) {
  var obj = _self.PlayerStarter.playerArray[0];
  if (obj) habit -= obj.player.paused() ? 0 : setting.time;
  if (habit >= 0) return setTimeout(totalTime, setting.time, habit);
  obj.player.pause();
  var $tips = $(".dialog-tips")
      .eq(0)
      .clone()
      .css("z-index", 3000)
      .appendTo(".video-study")
      .show(),
    html =
      '<div class="el-dialog__title" style="margin-top: 45px; text-align: center; color: red;">已达到挂机限制时间</div>';
  $tips.find(".el-dialog__title").text("智慧树网课助手提示").next().remove();
  $tips
    .find(".el-dialog__footer")
    .before(html)
    .find("button")
    .click(function () {
      $tips.remove();
    })
    .find("span")
    .text("明白了");
}

function checkToNext() {
  if (setting.habit < 0) return;
  var $tip = $(".video, .lessonItem, .file-item");
  if ($(".current_play .time_icofinish").length) {
    $tip
      .slice($tip.index($(".current_play")) + 1)
      .not(":has(.time_icofinish)")
      .eq(0)
      .click();
  } else if ($(".lessonItemActive .finish").length) {
    // _self.PlayerStarter.playerArray[0].callback.playerNext();
    $tip
      .slice($tip.index($(".lessonItemActive")) + 1)
      .not(":has(.finish)")
      .eq(0)
      .click();
  } else if (url.match("vod_room")) {
    $('.current_player:contains("100%") + li').click();
    // $('.finish_tishi').hasClass('disNo') || console.log('签到已完成');
  } else if ($(".active .icon-finish").length) {
    $tip
      .slice($tip.index($(".active")) + 1)
      .not(":has(.icon-finish)")
      .eq(0)
      .click();
  }
}

function doTest() {
  if (!$(".dialog-test").length) {
  } else if (setting.queue.length) {
    $(setting.queue.shift()).parent().click();
  } else if (!$(".item-topic.active").length) {
    $(".topic-item").eq(0).click();
  } else if ($(".error").length) {
    var tip = $(".answer span").text().match(/[A-Z]/g) || [];
    if (tip.length == 1)
      return $(".topic-option-item:contains(" + tip[0] + ")").click();
    $(".topic-option-item").each(function () {
      $.inArray($(this).text().slice(0, 1), tip) < 0 ==
        $(this).hasClass("active") && setting.queue.push(this);
    });
  } else if ($(".btn-next:enabled").length) {
    $(".btn-next:enabled").click();
  } else {
    $(".dialog-test .btn").click();
    _self.PlayerStarter.playerArray[0].player.play();
  }
}

function relieveLimit(doc) {
  if (!doc.oncut && !doc.onselectstart)
    return setTimeout(relieveLimit, 100, doc);
  doc.oncontextmenu =
    doc.onpaste =
    doc.oncopy =
    doc.oncut =
    doc.onselectstart =
      null;
}

function beforeFind() {
  setting.div = $(
    '<div class="jumbotron" style="border: 2px solid rgb(0, 0, 0); margin:5px; padding: 20px 20px 10px 20px;  width: 500px; position: fixed; top: 0; left: 0; z-index: 99999; background-color: ; overflow-x: auto;">' +
      '<span style="font-size: medium;"></span>' +
      '<div class="label-success" style="width:200px"><h3>正在搜索答案...<h3></div>' +
      '<button class="btn btn-primary" style="width:80px; height:35px;margin-right: 10px;">暂停答题</button>' +
      '<button class="btn btn-primary" style="margin-right: 10px;">重新查询</button>' +
      '<button class="btn btn-primary" style="margin-right: 10px;">折叠面板</button>' +
      '<button class="btn btn-primary" style="margin-top: 10px;display: none;">未作答题目</button>' +
      '<form style="margin: 2px 0;">' +
      '<h4><label class="label label-warning" >自定义答题范围:</label><h4>' +
      '<input class="form-control" name="num" type="number" min="1" placeholder="开始" style="width: 80px;" disabled>' +
      "<span>   </span> " +
      '<input class="form-control" name="max" type="number" min="1" placeholder="结束" style="width: 80px;" disabled>' +
      "</form>" +
      '<div style="max-height: 300px; overflow-y: auto;">' +
      '<table  class="table table-striped" style="font-size: 12px;">' +
      "<thead>" +
      "<tr>" +
      '<th style="width: 30px; min-width: 30px; font-weight: bold; text-align: center;">题号</th>' +
      '<th style="width: 60%; min-width: 130px; font-weight: bold; text-align: center;">题目(点击可复制)</th>' +
      '<th style="min-width: 130px; font-weight: bold; text-align: center;">答案(点击可复制)</th>' +
      "</tr>" +
      "</thead>" +
      '<tfoot style="display: none;">' +
      "<tr>" +
      '<th colspan="3" style="font-weight: bold; text-align: center;">答案提示框 已折叠</th>' +
      "</tr>" +
      "</tfoot>" +
      "<tbody>" +
      "<tr>" +
      '<td colspan="3" style="display: none;"></td>' +
      "</tr>" +
      "</tbody>" +
      "</table>" +
      "</div>" +
      "</div>"
  )
    .appendTo("body")
    .on("click", "button, td", function () {
      var len = $(this).prevAll("button").length;
      if (this.nodeName == "TD") {
        $(this).prev().length && GM_setClipboard($(this).text());
      } else if (len === 0) {
        if (setting.loop) {
          clearInterval(setting.loop);
          delete setting.loop;
          len = [false, "<h3>已赞停搜索<h3>", "继续答题"];
        } else {
          setting.loop = setInterval(findAnswer, setting.time);
          len = [true, "<h3>正在搜索答案...<h3>", "暂停答题"];
        }
        setting.div.find("input").attr("disabled", len[0]);
        setting.div
          .children("div:eq(0)")
          .html(function () {
            return $(this).data("html") || len[1];
          })
          .removeData("html");
        $(this).html(len[2]);
      } else if (len == 1) {
        location.reload();
      } else if (len == 2) {
        setting.div.find("tbody, tfoot").toggle();
      } else if (len == 3) {
        var $li = $(".el-scrollbar__wrap li"),
          $tip = $li.filter(".white, .yellow").eq(0);
        $tip.click().length
          ? setting.div.children("div:last").scrollTop(function () {
              var $tr = $("tbody tr", this).has(
                "td:nth-child(1):contains(" + $tip.text() + ")"
              );
              if (!$tr.length) return arguments[1];
              return $tr.offset().top - $tr.parents("table").offset().top; // $tr[0].offsetTop
            })
          : $(this).hide();
      }
    })
    .on("change", "input", function () {
      setting[this.name] = this.value.match(/^\d+$/)
        ? parseInt(this.value) - 1
        : -1;
      if (!this.value) setting[this.name] = this.name == "num" ? 0 : undefined;
    })
    .detach(setting.hide ? "*" : "html");
  setting.type = {
    单选题: 1,
    多选题: 2,
    填空题: 3,
    问答题: 4,
    "分析题/解答题/计算题/证明题": 5,
    "阅读理解(选择)/完型填空": 9,
    判断题: 14,
  };
  setting.lose = setting.num = setting.small = 0;
  $(document).keydown(function (event) {
    if (event.keyCode == 38) {
      setting.div.detach();
    } else if (event.keyCode == 40) {
      setting.div.appendTo("body");
    }
  });
  setting.loop = setInterval(findAnswer, setting.time, true);
  setInterval(function () {
    $(setting.queue.shift()).parent().click();
  }, 1e3);
}

function findAnswer(tip) {
  if (setting.queue.length) {
    return;
  } else if (tip && !$(".answerCard").length) {
    return setting.div
      .children("div:eq(0)")
      .data("html", "非自动答题页面")
      .siblings("button:eq(0)")
      .click();
  } else if (setting.max < 0 || setting.num < 0) {
    return setting.div
      .children("div:eq(0)")
      .data("html", '范围参数应为 <font color="red">正整数</font>')
      .siblings("button:eq(0)")
      .click();
  } else if (
    setting.num >= $(".subject_stem").length ||
    setting.num > setting.max
  ) {
    // setting.div.children('button:eq(3)').toggle(!!setting.lose);
    tip = setting.lose
      ? '<h4>共有 <font color="red">' +
        setting.lose +
        "</font> 道题目未完成<h4>"
      : "<h3>答题已完成<h3>";
    return setting.div
      .children("div:eq(0)")
      .data("html", tip)
      .siblings("button:eq(0), form")
      .hide()
      .click();
  } else if (!setting.curs.length) {
    setting.curs = $(".infoList span").map(function () {
      return $(this).text().trim();
    });
    if (!setting.curs.length) return;
  }
  var $TiMu = $(".subject_stem").eq(setting.num).parent(),
    $dom = $TiMu
      .find(".smallStem_describe")
      .eq(setting.small)
      .children("div")
      .slice(1, -1),
    question =
      filterStyle($dom) || filterStyle($TiMu.find(".subject_describe")),
    type = $TiMu
      .find(".subject_type")
      .text()
      .match(/【(.+)】|$/)[1];
  type = type ? setting.type[type] || 0 : -1;
  GM_xmlhttpRequest({
    method: setting.api[0].method,
    url: setting.api[0].url,
    headers: {
      "Content-type": "application/x-www-form-urlencoded",
    },
    data: "question=" + encodeURIComponent(question),
    timeout: setting.time,
    onload: function (xhr) {
      if (!setting.loop) {
      } else if (xhr.status == 200) {
        var obj =
          $.parseJSON(xhr.responseText.replace(/^操作数据失败!/, "")) || {};
        obj.answer = obj.data;
        if (obj.code) {
          setting.div.children("div:eq(0)").html("<h3>正在搜索答案...<h3>");
          var answer = obj.answer
            .replace(/&/g, "&amp;")
            .replace(/<([^i])/g, "&lt;$1");
          obj.answer = /^http/.test(answer)
            ? '<img src="' + obj.answer + '">'
            : obj.answer;
          $(
            "<tr>" +
              '<td style="text-align: center;">' +
              $TiMu.find(".subject_num").text().trim().replace(".", "") +
              "</td>" +
              '<td title="点击可复制">' +
              (question.match("<img")
                ? question
                : question.replace(/&/g, "&amp;").replace(/</g, "&lt")) +
              "</td>" +
              '<td title="点击可复制">' +
              (/^http/.test(answer) ? obj.answer : "") +
              answer +
              "</td>" +
              "</tr>"
          )
            .appendTo(setting.div.find("tbody"))
            .css("background-color", function () {
              $dom = $dom.length ? $dom.closest(".examPaper_subject") : $TiMu;
              if (fillAnswer($dom, obj, type)) return "";
              setting.div.children("button:eq(3)").show();
              return "rgba(0, 150, 136, 0.6)";
            });
          setting.small =
            ++setting.small < $TiMu.find(".smallStem_describe").length
              ? setting.small
              : (setting.num++, 0);
        } else {
          setting.div
            .children("div:eq(0)")
            .html(obj.answer || "服务器繁忙,正在重试...");
        }
        setting.div.children("span").html(obj.msg || "");
      } else if (xhr.status == 403) {
        var html = xhr.responseText.indexOf("{")
          ? "请求过于频繁,建议稍后再试"
          : $.parseJSON(xhr.responseText).answer;
        setting.div
          .children("div:eq(0)")
          .data("html", html)
          .siblings("button:eq(0)")
          .click();
      } else {
        setting.div.children("div:eq(0)").text("服务器异常,正在重试...");
      }
    },
    ontimeout: function () {
      setting.loop &&
        setting.div.children("div:eq(0)").text("服务器超时,正在重试...");
    },
  });
}

function fillAnswer($TiMu, obj, type) {
  var $div = $TiMu.find(".nodeLab"),
    str = String(obj.data).toCDB() || new Date().toString(),
    data = str.split(/#|\x01|\|/),
    state = setting.lose;
  // $div.find(':radio:checked').prop('checked', false);
  obj.code > 0 &&
    $div.each(function () {
      var $input = $("input", this)[0],
        tip =
          filterStyle(".node_detail", this).toCDB() || new Date().toString();
      if (tip.match(/^(正确|是|对|√|T|ri)$/)) {
        data.join().match(/(^|,)(正确|是|对|√|T|ri|right|true)(,|$)/) &&
          setting.queue.push($input);
      } else if (tip.match(/^(错误|否|错|×|F|wr)$/)) {
        data.join().match(/(^|,)(错误|否|错|×|F|wr|wrong|false)(,|$)/) &&
          setting.queue.push($input);
      } else if (type == 2) {
        Boolean($.inArray(tip, data) + 1 || str.indexOf(tip) + 1) ==
          $input.checked || setting.queue.push($input);
      } else {
        $.inArray(tip, data) + 1 && setting.queue.push($input);
      }
    });
  if (setting.queue.length) {
  } else if (/^(1|2|14)$/.test(type)) {
    var $input = $div.find("input");
    $input.is(":checked") ||
      (setting.none
        ? setting.queue.push($input[Math.floor(Math.random() * $input.length)])
        : setting.lose++);
  } else if (/^[3-5]$/.test(type)) {
    data = String(obj.data).split(/#|\x01|\|/);
    str = $TiMu.find("textarea").each(function (index) {
      index = (obj.code > 0 && data[index]) || this.value || "";
      this.value = index.trim();
      // if (this.value == this._value) return true;
      this.dispatchEvent(new Event("input"));
      this.dispatchEvent(new Event("blur"));
    }).length;
    (obj.code > 0 && data.length == str) || setting.none || setting.lose++;
  } else {
    setting.none || setting.lose++;
  }
  return state == setting.lose;
}

function filterStyle(dom, that) {
  var $dom = $(dom, that).clone().find("style").remove().end();
  return $dom
    .find("img[src]")
    .replaceWith(function () {
      return $("<p></p>").text('<img src="' + $(this).attr("src") + '">');
    })
    .end()
    .text()
    .trim();
}