头歌自动尝试正确答案

Automatically fetch answers for tasks

// ==UserScript==
// @name         头歌自动尝试正确答案
// @match        *://*.educoder.net/tasks/*
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Automatically fetch answers for tasks
// @author       Owwk
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  // 定义答案的所有可能组合
  const answers = [];
  const delayMs = 1;
  const isDelay = false;

  function fillAnswers(maxChar) {
    const options = [];
    for (let i = 'A'.charCodeAt(0); i <= maxChar.charCodeAt(0); i++) {
      options.push(String.fromCharCode(i));
    }
    for (let i = 1; i <= options.length; i++) {
      combinations(options, i).forEach(c => answers.push(c.join('')));
    }
    console.info("答案组合", answers)
  }

  function combinations(arr, k) {
    const result = [];
    const combination = (start, prefix) => {
      if (prefix.length === k) {
        result.push(prefix);
        return;
      }
      for (let i = start; i < arr.length; i++) {
        combination(i + 1, prefix.concat(arr[i]));
      }
    };
    combination(0, []);
    return result;
  }

  // 每秒获取一次目标元素,超时时间为5秒
  const interval = setInterval(() => {
    const target = document.querySelector('#task-left-panel > div.task-header > span');
    if (target) {
      clearInterval(interval);
      insertButton(target);
    }
  }, 1000);

  setTimeout(() => clearInterval(interval), 5000);

  function insertButton(target) {
    const button = document.createElement('button');
    button.textContent = 'Fetch Answer';
    button.style.marginRight = '10px';
    target.parentElement.insertBefore(button, target);

    button.addEventListener('click', () => {
      const userInput = prompt('请输入fetch字符串:');
      if (userInput) {
        try {
          const { url, options } = parseFetchString(userInput);
          attemptAllCombinations(url, options);
        } catch (e) {
          alert('无效的fetch字符串');
        }
      }
    });
  }

  function parseFetchString(fetchString) {
    const urlMatch = fetchString.match(/fetch\("([^"]+)"/);
    const optionsMatch = fetchString.match(/,\s*({[\s\S]*})\s*\)\s*;/);

    if (!urlMatch || !optionsMatch) {
      throw new Error('无法解析fetch字符串');
    }

    const url = urlMatch[1];
    const options = JSON.parse(optionsMatch[1]);
    return { url, options };
  }

  async function attemptAllCombinations(url, options) {
    // 解析 options.body 获取答案数组的长度
    let bodyObj = JSON.parse(options.body);
    const answerLength = bodyObj.answer.length;

    let maxChar = 'A';
    // 解析最大Char
    bodyObj.answer.forEach((val, index) => {
      if (val > maxChar) {
        maxChar = val;
      }
    });
    fillAnswers(maxChar);

    // 初始化 correctAnswers 数组,假设所有题目都是错误的
    let correctAnswers = Array(answerLength).fill(false);
    let num = 0;

    for (let answer of answers) {
      num++;
      bodyObj = modifyRequestBody(bodyObj, answer, correctAnswers);
      const newRequestOptions = { ...options, body: JSON.stringify(bodyObj) };
      const response = await sendRequest(url, newRequestOptions);

      console.info("尝试", num, bodyObj.answer)
      // 更新 correctAnswers 数组,标记已经正确的题目
      correctAnswers = updateCorrectAnswers(correctAnswers, response);
      console.info("结果", num, correctAnswers)
      if (isAllCorrect(response)) {
        alert('找到正确答案: ' + getAnswer(response).join(', '));
        break
      }
      if (isDelay) await delay(delayMs);
    }
  }


  function updateCorrectAnswers(correctAnswers, response) {
    // 遍历 response 中的 test_sets,更新 correctAnswers
    response.test_sets.forEach((test, index) => {
      if (test.result) {
        correctAnswers[index] = true;
      }
    });

    return correctAnswers;
  }


  function modifyRequestBody(body, answer, correctAnswers) {
    const bodyObj = body;

    // 仅在对应位置没有正确答案时才修改答案数组
    for (let i = 0; i < bodyObj.answer.length; i++) {
      if (!correctAnswers[i]) {
        bodyObj.answer[i] = answer;
      }
    }

    return bodyObj
  }


  function sendRequest(url, options) {
    Object.keys(options.headers).forEach(header => {
      if (header.startsWith('x-edu-')) {
        delete options.headers[header];
      }
    });

    return fetch(url, options)
      .then(response => response.json())
      .catch(error => console.error('Error:', error));
  }


  function isAllCorrect(response) {
    return response.test_sets.every(test => test.result);
  }

  function getAnswer(response) {
    return response.test_sets.map(val => val.actual_output)
  }

  function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
})();