NJU Helper

A helper for you to automate your life in NJU.

// ==UserScript==
// @name         NJU Helper
// @namespace    Flying-Tom/NJU-Helper
// @version      0.2.0
// @author       Flying-Tom
// @description  A helper for you to automate your life in NJU.
// @license      AGPL-3.0
// @icon         https://z1.ax1x.com/2023/11/21/pia2Gtg.png
// @match        https://authserver.nju.edu.cn/authserver/login*
// @match        http://ndyy.nju.edu.cn/*
// @match        https://zhtj.youth.cn/zhtj/
// @match        https://ehall.nju.edu.cn/ywtb-portal/official/index.html*
// @match        https://mail.smail.nju.edu.cn/cgi-bin/loginpage*
// @match        https://ehallapp.nju.edu.cn/gsapp/sys/wspjapp/*
// @match        https://epay.nju.edu.cn/epay/h5/*
// @match        https://zzbdjgz.nju.edu.cn/consumer/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/ort.min.js#sha256-CdOXaLKpsqac99HNMqEaQ4sKAPkm73Se4ux6F8hzSfM=
// @resource     ONNX_MODEL  https://cdn.jsdelivr.net/gh/Do1e/NJUcaptcha@main/model/checkpoints/nju_captcha.onnx
// @grant        GM_getResourceURL
// @grant        GM_info
// ==/UserScript==

(function () {
  'use strict';

  var _GM_getResourceURL = /* @__PURE__ */ (() => typeof GM_getResourceURL != "undefined" ? GM_getResourceURL : void 0)();
  var _GM_info = /* @__PURE__ */ (() => typeof GM_info != "undefined" ? GM_info : void 0)();
  function wechat_page_handler() {
    console.debug("Solving wechat page");
    if (window.location.href.includes("electric")) {
      return;
    } else if (window.location.href.includes("recharge")) {
      const styleElement = document.createElement("style");
      styleElement.textContent = `
          body {
              width: 40%;
              margin: 0 auto;
          }
        `;
      document.head.appendChild(styleElement);
    }
  }
  function waitForElm(selector) {
    return new Promise((resolve) => {
      if (document.querySelector(selector)) {
        return resolve(document.querySelector(selector));
      }
      const observer = new MutationObserver(() => {
        if (document.querySelector(selector)) {
          resolve(document.querySelector(selector));
          observer.disconnect();
        }
      });
      observer.observe(document.body, {
        childList: true,
        subtree: true
      });
    });
  }
  function waitForElms(selector) {
    return new Promise((resolve) => {
      if (document.querySelectorAll(selector).length > 0) {
        return resolve(document.querySelectorAll(selector));
      }
      const observer = new MutationObserver(() => {
        if (document.querySelectorAll(selector).length > 0) {
          resolve(document.querySelectorAll(selector));
          observer.disconnect();
        }
      });
      observer.observe(document.body, {
        childList: true,
        subtree: true
      });
    });
  }
  const charset = ["1", "2", "3", "4", "5", "6", "7", "8", "a", "b", "c", "d", "e", "f", "h", "k", "n", "p", "q", "x", "y", "z"];
  const resizeWidth = 176;
  const resizeHeight = 64;
  const charCount = 4;
  const numClasses = 22;
  const mean = [0.743, 0.7432, 0.7431];
  const std = [0.1917, 0.1918, 0.1917];
  let modelSession = null;
  let modelPromise = null;
  function waitForOrt() {
    return new Promise((resolve) => {
      if (typeof ort !== "undefined") {
        resolve();
        return;
      }
      const checkOrt = () => {
        if (typeof ort !== "undefined") {
          resolve();
        } else {
          setTimeout(checkOrt, 100);
        }
      };
      checkOrt();
    });
  }
  async function initializeONNXRuntime() {
    await waitForOrt();
    ort.env.wasm.wasmPaths = "https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/";
  }
  async function loadONNXModel() {
    if (modelSession) {
      return modelSession;
    }
    if (modelPromise) {
      return modelPromise;
    }
    modelPromise = (async () => {
      try {
        const modelURL = await _GM_getResourceURL("ONNX_MODEL");
        const response = await fetch(modelURL);
        if (!response.ok) {
          throw new Error(`下载ONNX模型失败,状态码: ${response.status}`);
        }
        const modelData = await response.arrayBuffer();
        const session = await ort.InferenceSession.create(modelData, {
          executionProviders: ["wasm"]
        });
        modelSession = session;
        return session;
      } catch (error) {
        console.error("ONNX模型加载失败:", error);
        throw error;
      }
    })();
    return modelPromise;
  }
  function preprocessImage(imageData) {
    const { data, width, height } = imageData;
    const float32Data = new Float32Array(3 * height * width);
    for (let i = 0; i < height * width; i++) {
      const r = data[i * 4] / 255;
      const g = data[i * 4 + 1] / 255;
      const b = data[i * 4 + 2] / 255;
      float32Data[i] = (r - mean[0]) / std[0];
      float32Data[i + height * width] = (g - mean[1]) / std[1];
      float32Data[i + 2 * height * width] = (b - mean[2]) / std[2];
    }
    return float32Data;
  }
  async function recognizeCaptcha(imageElement) {
    try {
      const canvas = document.createElement("canvas");
      canvas.width = resizeWidth;
      canvas.height = resizeHeight;
      const ctx = canvas.getContext("2d");
      if (!ctx) {
        throw new Error("无法创建Canvas上下文");
      }
      ctx.drawImage(imageElement, 0, 0, resizeWidth, resizeHeight);
      const imageData = ctx.getImageData(0, 0, resizeWidth, resizeHeight);
      const float32Data = preprocessImage(imageData);
      const session = await loadONNXModel();
      const inputTensor = new ort.Tensor("float32", float32Data, [1, 3, resizeHeight, resizeWidth]);
      const feeds = { "input": inputTensor };
      const results = await session.run(feeds);
      const outputTensor = results.output;
      const outputData = outputTensor.data;
      let text = "";
      for (let i = 0; i < charCount; i++) {
        const startIndex = i * numClasses;
        const endIndex = startIndex + numClasses;
        const classScores = Array.from(outputData.slice(startIndex, endIndex));
        let maxScore = -Infinity;
        let maxIndex = -1;
        for (let j = 0; j < classScores.length; j++) {
          if (classScores[j] > maxScore) {
            maxScore = classScores[j];
            maxIndex = j;
          }
        }
        text += charset[maxIndex];
      }
      return text;
    } catch (error) {
      console.error("验证码识别出错:", error);
      throw error;
    }
  }
  async function solveCAPTCHA(image_target, captcha_input_selectors, autoLogin) {
    try {
      const result = await recognizeCaptcha(image_target);
      console.debug("验证码识别结果:", result);
      const captchaInput = document.querySelector(captcha_input_selectors);
      if (captchaInput) {
        captchaInput.value = result;
      }
      if (autoLogin.enabled === true) {
        const intervalId = setInterval(function() {
          const usernameInput = document.querySelector(autoLogin.usernameSelectors);
          const passwordInput = document.querySelector(autoLogin.passwordSelectors);
          const submitButton = document.querySelector(autoLogin.submitSelectors);
          if (usernameInput && passwordInput && submitButton) {
            if (usernameInput.value && passwordInput.value) {
              submitButton.click();
              clearInterval(intervalId);
            }
          }
        }, 1e3);
      }
    } catch (error) {
      console.error("验证码识别失败:", error);
    }
  }
  function handleCaptcha(captcha_img_selectors, captcha_input_selectors, autoLogin) {
    void initializeONNXRuntime().then(() => {
      void waitForElm(captcha_img_selectors).then((data) => {
        const image_target = data;
        if (image_target.complete === true) {
          void solveCAPTCHA(image_target, captcha_input_selectors, autoLogin);
        } else {
          image_target.addEventListener("load", () => void solveCAPTCHA(image_target, captcha_input_selectors, autoLogin));
        }
      });
    }).catch((error) => {
      console.error("ONNX Runtime 初始化失败:", error);
    });
  }
  function njuauth_handler() {
    var _a;
    console.debug("Solving njuauth captcha");
    const inputElement = document.querySelector("input[name=dllt][value=userNamePasswordLogin]");
    if (inputElement) {
      inputElement.value = "mobileLogin";
    }
    const showPassElement = (_a = document.getElementsByClassName("showPass")) == null ? void 0 : _a[0];
    if (showPassElement) {
      showPassElement.click();
    }
    handleCaptcha("#captchaImg", "#captchaResponse", {
      enabled: true,
      // enabled: false,
      usernameSelectors: "input[name=username]",
      passwordSelectors: "input[type=password]",
      submitSelectors: "button[type=submit]"
    });
  }
  function njuhospital_handler() {
    console.debug("Solving nju hospital captcha");
    const inputElement = document.querySelector('input[name="NewWebYzm"]');
    if (inputElement) {
      inputElement.value = "";
    }
    handleCaptcha("#refreshCaptcha", 'input[name="NewWebYzm"]', {
      enabled: false
    });
  }
  function ehall_handler() {
    console.debug("Solving ehall captcha");
    const intervalId = setInterval(function() {
      if (document.querySelector(".header-user")) {
        const spanElement = document.evaluate("//span[contains(., '首页')]", document, null, XPathResult.ANY_TYPE).iterateNext();
        if (spanElement) {
          spanElement.click();
        }
        clearInterval(intervalId);
      }
      const btnLogin = document.querySelector(".btn-login");
      if (btnLogin) {
        btnLogin.click();
      }
    }, 200);
    setTimeout(function() {
      clearInterval(intervalId);
    }, 5e3);
  }
  function email_trust_handler() {
    console.debug("Solving email trust");
    const intervalId = setInterval(function() {
      const checkbox = document.getElementById("force_wx_scan_login_tc");
      if (checkbox) {
        checkbox.checked = true;
        clearInterval(intervalId);
      }
    }, 500);
  }
  function zhtj_handler() {
    console.debug("Solving zhtj captcha");
    handleCaptcha("#codeImage", "#yzm", {
      enabled: false
    });
  }
  function wspj_handler() {
    setInterval(checkForElement, 1e3);
    function checkForElement() {
      const element = document.querySelector("#pjfooter");
      if (element) {
        console.debug("Solving wspj page");
        void waitForElms(".bh-radio-label.paper_dx").then(
          (elems) => {
            const choices = elems;
            choices.forEach((elem) => {
              const choice = elem;
              const spanElement = choice.querySelector("span");
              const input = choice.querySelector("input");
              if (spanElement && input) {
                if (spanElement.textContent === "符合") {
                  input.checked = true;
                }
              }
            });
          }
        );
      } else {
        return;
      }
    }
  }
  function samrt_party_handler() {
    function search_and_pay() {
      console.debug("Try to search for input elements.");
      if (window.location.href.includes("partyCostPay")) {
        void waitForElms(".ivu-input-number-input").then((data) => {
          const feeInputElems = data;
          feeInputElems.forEach((inputElem) => {
            const fee = "0.2";
            inputElem.value = fee;
            inputElem.dispatchEvent(new Event("input", { bubbles: true }));
          });
        });
      }
    }
    void waitForElm("#app").then((data) => {
      const observer = new MutationObserver(search_and_pay);
      observer.observe(data, {
        childList: true,
        subtree: true
      });
    });
  }
  (function() {
    (function() {
      const matchIdx = _GM_info.script.matches.map((rule) => rule.replace(/\.|\*|\/|\?/g, (match) => ({ ".": "\\.", "*": ".*", "/": "\\/", "?": "\\?" })[match] ?? "")).map((rule) => new RegExp(rule)).map((regExp, index) => regExp.test(window.location.href) ? index : null).filter((index) => index != null).join().toString();
      const strategyLoad = {
        "0": njuauth_handler,
        // 南大统一身份认证自动登录
        "1": njuhospital_handler,
        // 南大校医院验证码
        "2": zhtj_handler,
        // 智慧团建验证码
        "3": ehall_handler,
        // 南大 ehall 自动登录
        "4": email_trust_handler,
        // 南大学生邮箱信任本机
        "5": wspj_handler
        // 南大ehall 课程评教
      };
      const strategyInstant = {
        "6": wechat_page_handler,
        // 南大信息门户 pc 端
        "7": samrt_party_handler
        // 智慧党建
      };
      if (matchIdx in strategyInstant) {
        const strategyInstantFunc = strategyInstant[matchIdx];
        strategyInstantFunc();
      } else if (matchIdx in strategyLoad) {
        const strategyLoadFunc = strategyLoad[matchIdx];
        if (document.readyState == "complete") {
          strategyLoadFunc();
        } else {
          window.addEventListener("load", strategyLoadFunc);
        }
      }
    })();
  })();

})();