// ==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';
}
});
})();