B站硬核答题AI答题

bilibili会员硬核答题

// ==UserScript==
// @name         B站硬核答题AI答题
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  bilibili会员硬核答题
// @author       BigShark667
// @match        https://www.bilibili.com/h5/senior-newbie/qa
// @run-at       document-end
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @grant        GM_notification
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.slim.min.js
// @license      Apache-2.0
// @connect      api.deepseek.com
// @connect      api.gptapi.us
// ==/UserScript==

"use strict";
/* eslint-disable no-underscore-dangle, @typescript-eslint/no-empty-function */
(async () => {
  // Prevent tab visibility detection
  function disableVisibilityEvents() {
    window.onblur = () => {};
    window.onfocus = () => {};
    document.onfocusin = () => {};
    document.onfocusout = () => {};
  }

  // Override visibility event listeners
  disableVisibilityEvents();
  document._addEventListener = document.addEventListener;
  document.addEventListener = (...argv) => {
    if (['visibilitychange', 'mozvisibilitychange', 'webkitvisibilitychange', 'msvisibilitychange'].includes(argv[0])) {
      return;
    }
    document._addEventListener(...argv);
  };

  document._removeEventListener = document.removeEventListener;
  document.removeEventListener = (...argv) => {
    if (['visibilitychange', 'mozvisibilitychange', 'webkitvisibilitychange', 'msvisibilitychange'].includes(argv[0])) {
      return;
    }
    document._removeEventListener(...argv);
  };

  window.onload = disableVisibilityEvents;

  // Configuration and state
  const API_CONFIG = {
    'ChatAnywhere': {
      url: 'https://api.gptapi.us/v1/chat/completions',
      model: 'gpt-4o-mini'
    },
    'DeepSeek': {
      url: 'https://api.deepseek.com/v1/chat/completions',
      model: 'deepseek-chat'
    }
  };

  let config = GM_getValue('API_CONFIG') || { key: '', provider: '' };
  let prevQuestion = '';
  let isRunning = false;
  let processedQuestions = []; // 新增:历史记录数组

  // Register menu commands
  GM_registerMenuCommand('设置 ChatAnywhere API 密钥', () => {
    setupApiKey('ChatAnywhere');
  });

  GM_registerMenuCommand('设置 DeepSeek API 密钥', () => {
    setupApiKey('DeepSeek');
  });

  GM_registerMenuCommand('启动', () => {
    if (!config.key) {
      notify('错误', '请先设置 API 密钥!');
      return;
    }

    if (isRunning) {
      notify('提示', '脚本已经在运行中');
      return;
    }

    console.clear();
    notify('提示', '启动后不要点击页面!');
    isRunning = true;
    document.dispatchEvent(new Event('click'));
  });

  GM_registerMenuCommand('停止', () => {
    isRunning = false;
    notify('提示', '脚本已停止');
  });

  // Setup functions
  function setupApiKey(provider) {
    const key = prompt(`请输入 ${provider} API 密钥:`, config.provider === provider ? config.key : '');
    if (key) {
      config = { key, provider };
      GM_setValue('API_CONFIG', config);
      notify('成功', `${provider} API 密钥已设置`);
    }
  }

  function notify(title, text) {
    GM_notification({
      title: '哔哩哔哩硬核会员搜题GPT',
      text,
      timeout: 5000
    });
  }

  // Main process functions
  document.addEventListener('click', startProcess);

  async function startProcess() {
    if (!isRunning) return;

    try {
      const delayTime = getRandomDelay(5000, 10000);
      console.log(`等待 ${delayTime / 1000} 秒后开始搜索下一题`);
      await sleep(delayTime);

      const [question, elem] = extractQuestion();
      if (!question) {
        console.log('未找到问题,稍后重试');
        return setTimeout(startProcess, 5000);
      }

      if (question === prevQuestion) {
        console.log('题目未变化,稍后重试');
        return setTimeout(startProcess, 5000);
      }

      prevQuestion = question;
      console.log('识别到问题:', question);

      const answer = await askGPT(question);
      const option = parseAnswer(answer);

      if (!option) {
        console.log('无法解析答案,随机选择');
        selectRandomOption();
      } else {
        console.log(`选择答案:${option}`);
        selectOption(option, elem);
      }
    } catch (error) {
      console.error('处理出错:', error);
      notify('错误', '处理题目时出错,稍后重试');
      setTimeout(startProcess, 10000);
    }
  }

  function extractQuestion() {
    const questionDivs = $('div.senior-question').toArray();
    for (const div of questionDivs) {
      const elements = $(div).find('.senior-question__qs, .senior-question__answer');
      if (elements.length === 0) continue;

      const questionText = elements.toArray()
        .map(v => $(v).text().trim())
        .join('\n')
        .replace(/\s+/g, ' ');  // 清理多余空格

      const hash = generateHash(questionText);

      if (!processedQuestions.includes(hash)) {
        processedQuestions.push(hash);
        // 保持历史记录最多50条
        if (processedQuestions.length > 50) {
          processedQuestions.shift();
        }
        return [questionText, div];
      }
    }
    return null;
  }

  function generateHash(str) {
    // 改进的djb2哈希算法
    let hash = 5381;
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = (hash * 33) ^ char;
    }
    return hash >>> 0; // 转换为无符号32位整数
  }

  async function askGPT(question, retry = 0) {
    try {
      return await callGPTAPI(`请仅给出答案\n${question}`);
    } catch (error) {
      if (retry >= 3) {
        throw new Error('API 请求失败次数过多');
      }
      console.log(`API 请求失败,${retry + 1}/3 次重试`);
      await sleep(5000);
      return askGPT(question, retry + 1);
    }
  }

  function parseAnswer(answer) {
    // First try to match a direct letter at the beginning or end
    let match = answer.match(/^\s*([A-D])[.\s:]|[.\s:]([A-D])\s*$/i);
    if (match) {
      return (match[1] || match[2]).toUpperCase();
    }

    // Then try to find any letter within the answer
    match = answer.match(/\b([A-D])[.\s:]/i);
    if (match) {
      return match[1].toUpperCase();
    }

    // Look for answer options by keyword
    const optionKeywords = {
      A: ['选A', '答案A', '答案是A', 'A选项', '选择A'],
      B: ['选B', '答案B', '答案是B', 'B选项', '选择B'],
      C: ['选C', '答案C', '答案是C', 'C选项', '选择C'],
      D: ['选D', '答案D', '答案是D', 'D选项', '选择D']
    };

    for (const [option, keywords] of Object.entries(optionKeywords)) {
      if (keywords.some(keyword => answer.includes(keyword))) {
        return option;
      }
    }

    return null;
  }

  function selectOption(option, questionDiv) {
    // const optionElement = $(questionDiv).find('span.senior-question__answer--icon:contains(${option})').parent()[0];
    const optionElement = $(questionDiv)
      .find(`span.senior-question__answer--icon:contains(${option})`)
      .closest('.senior-question__answer') // 更精确的父级选择
      .first()[0];
    if (optionElement) {
      simulateClick(optionElement);
    } else {
      console.log(`未找到选项 ${option},随机选择`);
      selectRandomOption();
    }
  }

  function selectRandomOption() {
    const options = $('span.senior-question__answer--icon').parent().toArray();
    if (options.length > 0) {
      const randomIndex = Math.floor(Math.random() * options.length);
      simulateClick(options[randomIndex]);
    }
  }

  function simulateClick(element) {
    if (!element) return;

    const rect = element.getBoundingClientRect();
    const randomX = rect.left + Math.random() * rect.width;
    const randomY = rect.top + Math.random() * rect.height;

    const clickEvent = new MouseEvent('click', {
      bubbles: true,
      cancelable: true,
      clientX: randomX,
      clientY: randomY,
    });

    element.dispatchEvent(clickEvent);
  }

  // API functions
  function callGPTAPI(question) {
    return new Promise((resolve, reject) => {
      if (!config.key || !config.provider) {
        return reject(new Error('API 配置不完整'));
      }

      const apiConfig = API_CONFIG[config.provider];
      if (!apiConfig) {
        return reject(new Error('未知的 API 提供商'));
      }

      const requestData = {
        "messages": [
          {
            "content": question,
            "role": "system"
          }
        ],
        "temperature": 0.7,
        "model": apiConfig.model,
        "stream": false,
        "response_format": {
          "type": "text"
        }
      };

      GM_xmlhttpRequest({
        method: 'POST',
        url: apiConfig.url,
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${config.key}`,
        },
        data: JSON.stringify(requestData),
        onload: function(response) {
          try {
            const data = JSON.parse(response.responseText);

            if (!data.choices || !data.choices[0] || !data.choices[0].message) {
              console.error('API 返回数据格式错误:', data);
              return reject(new Error('API 返回数据格式错误'));
            }

            const content = data.choices[0].message.content;
            console.log('API 返回结果:', content);
            resolve(content);
          } catch (error) {
            console.error('解析 API 响应失败:', error, response.responseText);
            reject(new Error('解析 API 响应失败'));
          }
        },
        onerror: function(error) {
          console.error('API 请求失败:', error);
          reject(error);
        },
        ontimeout: function() {
          console.error('API 请求超时');
          reject(new Error('API 请求超时'));
        }
      });
    });
  }

  // Utility functions
  function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  function getRandomDelay(min, max) {
    return Math.floor(Math.random() * (max - min + 1) + min);
  }
})();