SCNU Helper

华师砺儒云课堂与教务系统增强脚本

// ==UserScript==
// @name         SCNU Helper
// @namespace    scnu_helper
// @version      0.4.0
// @author       Ravi
// @description  华师砺儒云课堂与教务系统增强脚本
// @license      AGPL-3.0-only
// @match        https://moodle.scnu.edu.cn/*
// @match        https://jwxt.scnu.edu.cn/*
// @connect      api.siliconflow.cn
// @grant        GM_xmlhttpRequest
// @run-at       document-start
// ==/UserScript==

(function () {
  'use strict';

  var _GM_xmlhttpRequest = (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)();
  function video_zoom() {
    console.log("[Interceptor] 视频页面,准备拦截 tcplayer-video.js 脚本");
    let tcplayerProcessed = false;
    let tcplayerReady = false;
    const pendingMainScripts = [];
    const maxWaitMs = 7e3;
    function isMainScriptTag(tag) {
      try {
        if (!(tag instanceof HTMLScriptElement)) return false;
        const src = tag.getAttribute("src") || "";
        if (!src) return false;
        return /(^|\/)main(\.|$|-)/.test(src);
      } catch (_) {
        return false;
      }
    }
    function flushPendingMain(reason = "tcplayer 就绪") {
      if (!pendingMainScripts.length) return;
      console.log(`[Interceptor] 释放被拦截的 main.js(${pendingMainScripts.length} 个),原因:${reason}`);
      const target = document.head || document.documentElement || document.body;
      while (pendingMainScripts.length) {
        const node = pendingMainScripts.shift();
        try {
          target.appendChild(node);
        } catch (e) {
          console.warn("[Interceptor] 重新注入 main.js 失败:", e);
        }
      }
    }
    function interceptMainScriptsOnce() {
      const candidates = Array.from(document.querySelectorAll("script[src]")).filter(isMainScriptTag);
      for (const s of candidates) {
        if (tcplayerReady) {
          continue;
        }
        try {
          console.log("[Interceptor] 拦截到依赖 tcplayer 的 main.js:", s.src);
          s.remove();
          pendingMainScripts.push(s);
        } catch (e) {
          console.warn("[Interceptor] 移除 main.js 失败:", e);
        }
      }
    }
    function processTcplayerIfPresent() {
      const playerScriptTag = document.querySelector('script[src*="tcplayer-video.js"]');
      if (playerScriptTag && !tcplayerProcessed) {
        const originalSrc = playerScriptTag.src;
        console.log("[Interceptor] 拦截到 tcplayer-video.js:", originalSrc);
        playerScriptTag.remove();
        tcplayerProcessed = true;
        _GM_xmlhttpRequest({
          method: "GET",
          url: originalSrc,
          onload: function(response) {
            if (response.status === 200) {
              let modifiedCode = response.responseText;
              modifiedCode = modifiedCode.replace(
                "var time = Math.round(this.viewTotalTime / 1000)",
                "this.viewTotalTime = 99999;\nvar time = Math.round(this.viewTotalTime / 1000)"
              );
              console.log("[Interceptor] 已修改 tcplayer-video.js 内容,准备注入");
              const newScript = document.createElement("script");
              newScript.textContent = modifiedCode;
              newScript.type = "text/javascript";
              (document.head || document.documentElement).appendChild(newScript);
              console.log("[Interceptor] 修改后的 tcplayer-video.js 已注入");
              tcplayerReady = true;
              flushPendingMain("tcplayer 注入完成");
            } else {
              console.error("[Interceptor] 请求 tcplayer-video.js 失败:", response.status);
              flushPendingMain("请求 tcplayer 失败");
            }
          },
          onerror: function(error) {
            console.error("[Interceptor] 请求 tcplayer-video.js 出错:", error);
            flushPendingMain("请求 tcplayer 出错");
          }
        });
      }
    }
    const observer = new MutationObserver(() => {
      interceptMainScriptsOnce();
      processTcplayerIfPresent();
    });
    observer.observe(document.documentElement, { childList: true, subtree: true });
    interceptMainScriptsOnce();
    processTcplayerIfPresent();
    setTimeout(() => {
      if (!tcplayerReady && pendingMainScripts.length) {
        console.warn("[Interceptor] 等待 tcplayer 超时,释放 main.js 以避免页面卡死");
        flushPendingMain("等待超时");
      }
    }, maxWaitMs);
  }
  function ai_answer() {
    console.log("[AI Answer] 作答页面,准备运行 AI 作答脚本");
    const apiKey = getApiKey();
    if (!apiKey) {
      console.error("[AI Answer] 未提供 API Key,已取消");
      return;
    }
    const questions = extractQuestionsWithOptions();
    if (!questions.length) {
      console.error("[AI Answer] 未能从页面提取题目与选项");
      return;
    }
    console.log(`[AI Answer] 共提取到 ${questions.length} 道题`);
    let chain = Promise.resolve();
    questions.forEach((q, idx) => {
      chain = chain.then(() => {
        console.log(`
[AI Answer] 第 ${idx + 1} 题:
${q}`);
        return callSiliconFlowOnce(apiKey, q).then((answer) => {
          const letter = (answer || "").trim().toUpperCase().replace(/[^A-Z]/g, "").charAt(0);
          let div = document.createElement("div");
          if (!letter) {
            console.warn(`[AI Answer] 第 ${idx + 1} 题:未解析到有效选项字母,原始返回:`, answer);
            div.innerText = String(answer ?? "");
          } else {
            console.log(`[AI Answer] 第 ${idx + 1} 题模型答案:${letter}`);
            div.innerText = letter;
          }
          document.querySelectorAll('[class^="info"]')[idx]?.appendChild(div);
        }).catch((err) => {
          console.error(`[AI Answer] 第 ${idx + 1} 题请求失败:`, err);
        });
      });
    });
  }
  function getApiKey() {
    try {
      const keyInStore = localStorage.getItem("sf_api_key");
      if (keyInStore && keyInStore.trim()) return keyInStore.trim();
    } catch (_) {
    }
    const input = window.prompt("请输入 SiliconFlow API Key(仅提示一次,将保存在本地浏览器):");
    const key = (input || "").trim();
    if (key) {
      try {
        localStorage.setItem("sf_api_key", key);
      } catch (_) {
      }
      return key;
    }
    return "";
  }
  function extractQuestionsWithOptions() {
    const nodes = Array.from(document.querySelectorAll('[class^="formulation clearfix"]'));
    const results = [];
    for (const el of nodes) {
      results.push(el.innerText);
    }
    return results;
  }
  function callSiliconFlowOnce(apiKey, question) {
    const url = "https://api.siliconflow.cn/v1/chat/completions";
    const headers = {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json"
    };
    const data = {
      model: "THUDM/GLM-4-9B-0414",
      messages: [
        {
          role: "system",
          content: "能力与角色:你是一位答题助手。\n背景信息:你会得到一个题目和多个选项。\n指令:你要仔细思考问题,并从下面的几个选项中选择你认为正确的那个。\n输出风格:你无需给出推理过程以及任何解释。你只需要回答正确选项对应的字母,不得回答任何多余的文字,不得添加任何的标点符号。\n输出范围:我希望你仅仅回答一个字母。"
        },
        { role: "user", content: question }
      ],
      enable_thinking: false,
      temperature: 0.2
    };
    return new Promise((resolve, reject) => {
      _GM_xmlhttpRequest({
        method: "POST",
        url,
        headers,
        data: JSON.stringify(data),
        timeout: 2e4,
        onload: (resp) => {
          try {
            if (resp.status >= 200 && resp.status < 300) {
              const json = JSON.parse(resp.responseText || "{}");
              const content = json?.choices?.[0]?.message?.content;
              if (typeof content === "string" && content.trim()) {
                resolve(content);
              } else {
                reject(new Error("响应不包含有效内容"));
              }
            } else {
              reject(new Error(`HTTP ${resp.status}`));
            }
          } catch (e) {
            reject(e);
          }
        },
        onerror: (err) => reject(err)
      });
    });
  }
  function remove_timeinterval() {
    console.log("[Interceptor] 教务系统页面,准备移除倒计时限制");
    new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        mutation.addedNodes.forEach((node) => {
          if (node.tagName === "SCRIPT" && !node.src) {
            node.textContent = node.textContent.replace(
              "var count 	= (''==null||''=='')?((''==null||''=='')?5:''):'';",
              "var count = 0;"
            );
            console.log("[Interceptor] 修改后的代码已注入");
          }
        });
      });
    }).observe(document.documentElement, { childList: true, subtree: true });
  }
  const domain = window.location.hostname;
  const path = window.location.pathname;
  if (domain === "moodle.scnu.edu.cn") {
    if (path.includes("fsresource")) {
      video_zoom();
    } else if (path.includes("quiz/attempt.php")) {
      ai_answer();
    }
  } else if (domain === "jwxt.scnu.edu.cn") {
    if (path.includes("index_initMenu")) {
      remove_timeinterval();
    }
  }

})();