Python123

一个用于获取 Python123 选择题解析或答案的,接入大模型完成程序设计题的脚本

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Python123
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  一个用于获取 Python123 选择题解析或答案的,接入大模型完成程序设计题的脚本
// @author       forxk
// @match        *://python123.io/*
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  // 创建一个显示数据包的窗体
  const packetContainer = document.createElement('div');
  packetContainer.style.position = 'fixed';
  packetContainer.style.top = '10px';
  packetContainer.style.right = '10px';
  packetContainer.style.width = '350px';
  packetContainer.style.height = '500px';
  packetContainer.style.overflowY = 'scroll';
  packetContainer.style.backgroundColor = '#f9f9f9';
  packetContainer.style.border = '1px solid #ccc';
  packetContainer.style.borderRadius = '8px';
  packetContainer.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
  packetContainer.style.zIndex = '9999';
  packetContainer.style.fontSize = '14px';
  packetContainer.style.resize = 'both'; // 允许拖动四周边框调整大小
  packetContainer.style.overflow = 'auto';
  packetContainer.style.minWidth = '200px'; // 设置最小宽度
  packetContainer.style.minHeight = '200px'; // 设置最小高度
  document.body.appendChild(packetContainer);

  // 创建控制面板
  const controlPanel = document.createElement('div');
  controlPanel.style.position = 'sticky';
  controlPanel.style.top = '0';
  controlPanel.style.backgroundColor = '#e0e0e0';
  controlPanel.style.borderBottom = '1px solid #ccc';
  controlPanel.style.width = '100%';
  controlPanel.style.display = 'flex';
  controlPanel.style.flexDirection = 'column';
  controlPanel.style.padding = '5px';
  controlPanel.style.borderTopLeftRadius = '8px';
  controlPanel.style.borderTopRightRadius = '8px';
  controlPanel.style.zIndex = '10000'; // 确保控制面板在最上层
  packetContainer.appendChild(controlPanel);

  // 创建按钮容器
  const buttonContainer = document.createElement('div');
  buttonContainer.style.display = 'flex';
  buttonContainer.style.justifyContent = 'space-between';
  buttonContainer.style.marginBottom = '5px';
  controlPanel.appendChild(buttonContainer);

  const moveButton = document.createElement('button');
  moveButton.textContent = 'Move';
  moveButton.style.marginRight = '5px';
  moveButton.style.padding = '5px 10px';
  moveButton.style.border = 'none';
  moveButton.style.borderRadius = '4px';
  moveButton.style.backgroundColor = 'rgb(96, 206, 179)';
  moveButton.style.color = 'white';
  moveButton.style.cursor = 'pointer';
  buttonContainer.appendChild(moveButton);

  const minimizeButton = document.createElement('button');
  minimizeButton.textContent = 'Minimize';
  minimizeButton.style.padding = '5px 10px';
  minimizeButton.style.border = 'none';
  minimizeButton.style.borderRadius = '4px';
  minimizeButton.style.backgroundColor = 'rgb(96, 206, 179)';
  minimizeButton.style.color = 'white';
  minimizeButton.style.cursor = 'pointer';
  buttonContainer.appendChild(minimizeButton);

  const toggleButton = document.createElement('button');
  toggleButton.textContent = 'Toggle';
  toggleButton.style.padding = '5px 10px';
  toggleButton.style.border = 'none';
  toggleButton.style.borderRadius = '4px';
  toggleButton.style.backgroundColor = 'rgb(96, 206, 179)';
  toggleButton.style.color = 'white';
  toggleButton.style.cursor = 'pointer';
  buttonContainer.appendChild(toggleButton);

  // 创建模型选择容器
  const modelContainer = document.createElement('div');
  modelContainer.style.display = 'flex';
  modelContainer.style.justifyContent = 'space-between';
  modelContainer.style.marginBottom = '5px';
  controlPanel.appendChild(modelContainer);

  const modelSelect = document.createElement('select');
  modelSelect.style.padding = '5px 10px';
  modelSelect.style.border = 'none';
  modelSelect.style.borderRadius = '4px';
  modelSelect.style.backgroundColor = 'rgb(96, 206, 179)';
  modelSelect.style.color = 'white';
  modelSelect.style.cursor = 'pointer';
  const models = [
    'THUDM/glm-4-9b-chat',
    'Qwen/Qwen2.5-72B-Instruct',
    'deepseek-ai/DeepSeek-V2.5',
    'deepseek-ai/DeepSeek-V2-Chat',
    'Qwen/Qwen2.5-7B-Instruct'
  ];
  models.forEach(model => {
    const option = document.createElement('option');
    option.value = model;
    option.textContent = model;
    modelSelect.appendChild(option);
  });
  modelContainer.appendChild(modelSelect);

  // 从 localStorage 中读取并设置模型选择
  const savedModel = localStorage.getItem('selectedModel');
  if (savedModel) {
    modelSelect.value = savedModel;
  }

  // 创建搜索框容器
  const searchContainer = document.createElement('div');
  searchContainer.style.display = 'flex';
  searchContainer.style.justifyContent = 'space-between';
  controlPanel.appendChild(searchContainer);

  // 创建搜索框
  const searchInput = document.createElement('input');
  searchInput.type = 'text';
  searchInput.placeholder = '搜索内容';
  searchInput.style.padding = '5px 10px';
  searchInput.style.border = 'none';
  searchInput.style.borderRadius = '4px';
  searchInput.style.flexGrow = '1';
  searchContainer.appendChild(searchInput);

  // 监听搜索框输入事件
  searchInput.addEventListener('input', function () {
    const searchText = searchInput.value.toLowerCase();
    const packets = packetContainer.querySelectorAll('div.packet');
    packets.forEach(packet => {
      const content = packet.textContent.toLowerCase();
      if (content.includes(searchText)) {
        packet.style.display = 'block';
      } else {
        packet.style.display = 'none';
      }
    });
  });

  // 创建API Key输入框和按钮
  const apiKeyContainer = document.createElement('div');
  apiKeyContainer.style.display = 'flex';
  apiKeyContainer.style.justifyContent = 'space-between';
  apiKeyContainer.style.marginBottom = '5px';
  controlPanel.appendChild(apiKeyContainer);

  const apiKeyInput = document.createElement('input');
  apiKeyInput.type = 'text';
  apiKeyInput.placeholder = 'Enter API Key';
  apiKeyInput.style.padding = '5px 10px';
  apiKeyInput.style.border = 'none';
  apiKeyInput.style.borderRadius = '4px';
  apiKeyInput.style.flexGrow = '1';
  apiKeyContainer.appendChild(apiKeyInput);

  const saveApiKeyButton = document.createElement('button');
  saveApiKeyButton.textContent = 'Save API Key';
  saveApiKeyButton.style.padding = '5px 10px';
  saveApiKeyButton.style.border = 'none';
  saveApiKeyButton.style.borderRadius = '4px';
  saveApiKeyButton.style.backgroundColor = 'rgb(96, 206, 179)';
  saveApiKeyButton.style.color = 'white';
  saveApiKeyButton.style.cursor = 'pointer';
  apiKeyContainer.appendChild(saveApiKeyButton);

  // 从 localStorage 中读取并设置API Key
  const savedApiKey = localStorage.getItem('apiKey');
  if (savedApiKey) {
    apiKeyInput.value = savedApiKey;
  }

  saveApiKeyButton.onclick = function () {
    const apiKey = apiKeyInput.value;
    localStorage.setItem('apiKey', apiKey);
    alert('API Key saved!');
  };

  let isMinimized = false;
  minimizeButton.onclick = function () {
    if (isMinimized) {
      packetContainer.style.height = '500px';
      minimizeButton.textContent = 'Minimize';
    } else {
      packetContainer.style.height = '30px';
      minimizeButton.textContent = 'Maximize';
    }
    isMinimized = !isMinimized;
  };

  toggleButton.onclick = function () {
    if (packetContainer.style.display === 'none') {
      packetContainer.style.display = 'block';
    } else {
      packetContainer.style.display = 'none';
    }
  };

  modelSelect.onchange = function () {
    localStorage.setItem('selectedModel', modelSelect.value);
    refreshData();
  };

  // 使窗口可通过拖动Move按钮移动
  moveButton.onmousedown = function (event) {
    event.preventDefault();
    let shiftX = event.clientX - packetContainer.getBoundingClientRect().left;
    let shiftY = event.clientY - packetContainer.getBoundingClientRect().top;

    function moveAt(pageX, pageY) {
      packetContainer.style.left = pageX - shiftX + 'px';
      packetContainer.style.top = pageY - shiftY + 'px';
    }

    function onMouseMove(event) {
      moveAt(event.pageX, event.pageY);
    }

    document.addEventListener('mousemove', onMouseMove);

    document.onmouseup = function () {
      document.removeEventListener('mousemove', onMouseMove);
      document.onmouseup = null;
    };
  };

  moveButton.ondragstart = function () {
    return false;
  };

  function displayPacket(name, content, explanationContent, answer, description, aiResponse, type) {
    const packetElement = document.createElement('div');
    packetElement.classList.add('packet');
    packetElement.style.marginBottom = '10px';
    packetElement.style.borderBottom = '1px solid #ccc';
    packetElement.style.paddingBottom = '10px';
    packetElement.style.padding = '10px';
    packetElement.style.backgroundColor = 'white';
    packetElement.style.borderRadius = '4px';
    packetElement.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.1)';

    const nameElement = document.createElement('div');
    nameElement.textContent = `Name: ${name}`;
    nameElement.style.fontWeight = 'bold';
    nameElement.style.marginBottom = '5px';
    packetElement.appendChild(nameElement);

    if (content) {
      const contentElement = document.createElement('div');
      contentElement.textContent = `Content: ${content}`;
      contentElement.style.marginBottom = '5px';
      packetElement.appendChild(contentElement);
    }

    if (explanationContent) {
      const explanationElement = document.createElement('div');
      explanationElement.textContent = `Explanation: ${explanationContent}`;
      explanationElement.style.marginBottom = '5px';
      packetElement.appendChild(explanationElement);
    }

    if (answer) {
      const answerElement = document.createElement('div');
      answerElement.textContent = `Answer: ${answer}`;
      answerElement.style.marginBottom = '5px';
      packetElement.appendChild(answerElement);
    }

    if (description) {
      const descriptionElement = document.createElement('div');
      descriptionElement.textContent = `Description: ${description}`;
      descriptionElement.style.marginBottom = '5px';
      packetElement.appendChild(descriptionElement);
    }

    if (type) {
      const typeElement = document.createElement('div');
      typeElement.textContent = `Type: ${type}`;
      typeElement.style.marginBottom = '5px';
      packetElement.appendChild(typeElement);
    }

    if (aiResponse) {
      const aiResponseElement = document.createElement('pre');
      aiResponseElement.style.marginTop = '10px';
      aiResponseElement.style.padding = '10px';
      aiResponseElement.style.backgroundColor = '#f0f0f0';
      aiResponseElement.style.borderRadius = '4px';
      aiResponseElement.textContent = aiResponse;
      packetElement.appendChild(aiResponseElement);
    }

    packetContainer.appendChild(packetElement);
  }

  function extractAndDisplayData(json) {
    json.data.forEach(item => {
      const name = item.name || 'N/A';
      const content = item.content ? stripHtml(item.content) : 'N/A';
      const explanationContent = item.explanation_content ? stripHtml(item.explanation_content) : null;
      const answer = item.answer || null;
      const description = item.description || null;
      const type = item.type || 'N/A'; // 解析出 type 类型

      // 检查是否能从Content中找到答案
      let answerText = null;
      if (answer && content) {
        try {
          const contentArray = JSON.parse(content);
          answerText = contentArray.find(item => item[0] == answer)[1];
        } catch (e) {
          console.error('Failed to parse content or find answer:', e);
        }
      }

      if (answerText) {
        displayPacket(name, content, explanationContent, answer, description, null, type);
      } else {
        const apiKey = localStorage.getItem('apiKey');
        if (!apiKey) {
          displayPacket(name, content, explanationContent, answer, description, 'API Key is missing, cannot call AI model.', type);
        } else {
          sendToAI(name, content, explanationContent, answer, description, type, (aiResponse) => {
            displayPacket(name, content, explanationContent, answer, description, aiResponse, type);
          });
        }
      }
    });
  }

  function stripHtml(html) {
    const tempDiv = document.createElement('div');
    tempDiv.innerHTML = html;
    return tempDiv.textContent || tempDiv.innerText || '';
  }

  function sendToAI(name, content, explanationContent, answer, description, type, callback) {
    const apiKey = localStorage.getItem('apiKey');
    if (!apiKey) {
      alert('Please enter your API Key.');
      return;
    }

    const prompt = `Name: ${name}\nContent: ${content}\nExplanation: ${explanationContent}\nAnswer: ${answer}\nDescription: ${description}\nQuestion Type: ${type}`;
    const options = {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: modelSelect.value,
        messages: [
          { role: 'system', content: '你现在是一个 Python 程序助理,下面我将向你输入题目,题目的类型由“Question Type”决定,如果是“Choice”请输出完整选项;如果是"programming",请你按照要求输出Python代码,请不要输出md格式的代码,而是直接输出,并且输出完整的注释,以便我能直接复制粘贴' },
          { role: 'user', content: prompt }
        ]
      })
    };

    fetch('https://api.siliconflow.cn/v1/chat/completions', options)
      .then(response => response.json())
      .then(response => {
        console.log('AI Response:', response);
        const responseContent = response.choices && response.choices[0] && response.choices[0].message && response.choices[0].message.content;
        callback(responseContent || 'No response');
      })
      .catch(err => {
        console.error('AI Request Error:', err);
        callback('Error fetching AI response');
      });
  }

  function refreshData() {
    // 重新获取数据并刷新显示
    const event = new Event('keydown');
    event.ctrlKey = true;
    event.key = 'q';
    document.dispatchEvent(event);
  }

  // 正则表达式匹配URL
  const urlPattern = /\/api\/v1\/student\/courses\/\d+\/groups\/\d+\/problems/;

  // 拦截XMLHttpRequest
  (function (open) {
    XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
      console.log('XMLHttpRequest open called with URL:', url);
      this.addEventListener('readystatechange', function () {
        if (this.readyState === 4) {
          console.log('XMLHttpRequest readyState:', this.readyState, 'status:', this.status, 'URL:', url);
          if (this.status === 200 && urlPattern.test(url)) {
            console.log('Intercepted Prombles packet:', this.responseText);
            try {
              const jsonResponse = JSON.parse(this.responseText);
              extractAndDisplayData(jsonResponse);
            } catch (e) {
              console.error('Failed to parse JSON response:', e);
            }
          }
        }
      }, false);
      open.call(this, method, url, async, user, password);
    };
  })(XMLHttpRequest.prototype.open);

  // 拦截Fetch API
  (function (fetch) {
    window.fetch = function () {
      console.log('Fetch called with arguments:', arguments);
      return fetch.apply(this, arguments).then(function (response) {
        console.log('Fetch response URL:', response.url);
        if (urlPattern.test(response.url)) {
          response.clone().text().then(function (text) {
            console.log('Intercepted Prombles packet:', text);
            try {
              const jsonResponse = JSON.parse(text);
              extractAndDisplayData(jsonResponse);
            } catch (e) {
              console.error('Failed to parse JSON response:', e);
            }
          });
        }
        return response;
      });
    };
  })(window.fetch);

  // 添加快捷键监听器
  document.addEventListener('keydown', function (event) {
    if (event.ctrlKey && event.key === 'p') {
      packetContainer.style.display = 'none';
    }
    if (event.ctrlKey && event.key === 'q') {
      packetContainer.style.display = 'block';
    }
  });
})();