// ==UserScript==
// @name 厦门理工高校邦考试AI助手 (DeepSeek)
// @namespace https://github.com/Wu557666/gaoxiaobangai
// @version 1.2.0
// @description 精准适配高校邦考试/测验页面结构,支持题目、选项自动提取与AI答题
// @author Wu557666
// @icon https://favicon.im/xmut.gaoxiaobang.com?size=128
// @match https://xmut.class.gaoxiaobang.com/class/*/exam/*
// @match https://xmut.class.gaoxiaobang.com/class/*/quiz/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @connect api.deepseek.com
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// ========== 默认配置 ==========
const DEFAULT_API_URL = 'https://api.deepseek.com/chat/completions';
const DEFAULT_MODEL = 'deepseek-chat';
// ========== 存储键名 ==========
const STORAGE_API_KEY = 'deepseek_api_key';
const STORAGE_API_URL = 'deepseek_api_url';
const STORAGE_MODEL = 'deepseek_model';
// ========== 获取/设置配置 ==========
function getApiKey() {
return GM_getValue(STORAGE_API_KEY, '');
}
function setApiKey(key) {
GM_setValue(STORAGE_API_KEY, key);
}
function getApiUrl() {
return GM_getValue(STORAGE_API_URL, DEFAULT_API_URL);
}
function setApiUrl(url) {
GM_setValue(STORAGE_API_URL, url);
}
function getModel() {
return GM_getValue(STORAGE_MODEL, DEFAULT_MODEL);
}
function setModel(model) {
GM_setValue(STORAGE_MODEL, model);
}
// ========== 页面内弹窗配置 ==========
function showSettingsPanel() {
const oldOverlay = document.getElementById('ai-settings-overlay');
if (oldOverlay) oldOverlay.remove();
const apiKey = getApiKey();
const apiUrl = getApiUrl();
const model = getModel();
const overlay = document.createElement('div');
overlay.id = 'ai-settings-overlay';
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:99999;display:flex;align-items:center;justify-content:center;';
const panel = document.createElement('div');
panel.id = 'ai-settings-panel';
panel.style.cssText = 'background:white;padding:24px;border-radius:16px;box-shadow:0 10px 40px rgba(0,0,0,0.3);width:400px;max-width:90vw;font-family:Arial,sans-serif;';
panel.innerHTML = `
🤖 考试 AI 助手设置
请输入你的 DeepSeek API Key(点击获取)
高级设置(可选)
`;
overlay.appendChild(panel);
document.body.appendChild(overlay);
document.getElementById('ai-settings-cancel').onclick = () => {
overlay.remove();
if (!getApiKey()) {
const tip = document.createElement('div');
tip.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:9999;background:#ff9800;color:white;padding:10px 15px;border-radius:8px;';
tip.innerText = '⚠️ 未设置 API Key,无法使用';
document.body.appendChild(tip);
setTimeout(() => tip.remove(), 3000);
}
};
document.getElementById('ai-settings-save').onclick = () => {
const newKey = document.getElementById('ai-api-key-input').value.trim();
const newUrl = document.getElementById('ai-api-url-input').value.trim();
const newModel = document.getElementById('ai-model-input').value.trim();
if (!newKey) {
alert('❌ API Key 不能为空!');
return;
}
setApiKey(newKey);
if (newUrl) setApiUrl(newUrl);
if (newModel) setModel(newModel);
overlay.remove();
alert('✅ 配置已保存!现在可以使用 AI 答题了。');
location.reload();
};
}
// 注册菜单命令
GM_registerMenuCommand('⚙️ 打开设置面板', showSettingsPanel);
GM_registerMenuCommand('📋 查看当前配置', () => {
const key = getApiKey();
const url = getApiUrl();
const model = getModel();
const keyPreview = key ? key.slice(0, 8) + '...' + key.slice(-4) : '未设置';
alert(`当前配置:\n\nAPI Key: ${keyPreview}\nAPI 地址: ${url}\n模型: ${model}`);
});
// ========== 初始化检查 ==========
const apiKey = getApiKey();
if (!apiKey) {
console.warn('⚠️ 未设置 DeepSeek API Key,正在打开设置面板...');
setTimeout(showSettingsPanel, 1000);
return;
}
console.log('✅ DeepSeek 配置已加载');
// ========== 获取本页所有题目区块 ==========
function getAllQuestions() {
const questions = [];
// 优先通过 .question-item 容器来查找,结构更可靠
const containers = document.querySelectorAll('.question-item, .exam-item, .quiz-item, .question');
if (containers.length > 0) {
containers.forEach(container => {
const titleEl = container.querySelector('.quiz-title');
const answerWap = container.querySelector('.answer-wap');
if (titleEl && answerWap) {
// 提取题目文本:排除题型图标和索引序号
let questionText = '';
const nodes = titleEl.childNodes;
for (let node of nodes) {
if (node.nodeType === Node.TEXT_NODE) {
questionText += node.nodeValue;
} else if (node.nodeType === Node.ELEMENT_NODE) {
// 忽略题型图标(.quiz-type)和索引(.gxb-icon-index)
if (!node.classList.contains('quiz-type') && !node.classList.contains('gxb-icon-index')) {
questionText += node.innerText;
}
}
}
questionText = questionText.trim();
if (questionText) {
questions.push({
titleElement: titleEl,
answerElement: answerWap,
text: questionText
});
}
}
});
}
if (questions.length > 0) return questions;
// 降级:没有容器时,尝试直接匹配 .quiz-title 和 .answer-wap
const titles = document.querySelectorAll('.quiz-title');
const answerWaps = document.querySelectorAll('.answer-wap');
if (titles.length === answerWaps.length && titles.length > 0) {
for (let i = 0; i < titles.length; i++) {
const titleEl = titles[i];
const answerEl = answerWaps[i];
// 排除题型图标和索引
let questionText = '';
const nodes = titleEl.childNodes;
for (let node of nodes) {
if (node.nodeType === Node.TEXT_NODE) {
questionText += node.nodeValue;
} else if (node.nodeType === Node.ELEMENT_NODE) {
if (!node.classList.contains('quiz-type') && !node.classList.contains('gxb-icon-index')) {
questionText += node.innerText;
}
}
}
questionText = questionText.trim();
if (questionText) {
questions.push({
titleElement: titleEl,
answerElement: answerEl,
text: questionText
});
}
}
}
return questions;
}
// ========== 提取选项 ==========
function extractOptionsFromAnswerWap(answerWap) {
const options = [];
if (!answerWap) return options;
const answerItems = answerWap.querySelectorAll('.answer');
answerItems.forEach(item => {
// 选项字母可能直接作为文本节点,也可能被包裹
let letter = '';
let content = '';
// 尝试获取字母(通常是 A. B. 等)
const childNodes = item.childNodes;
for (let node of childNodes) {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.nodeValue.trim();
if (/^[A-Z]\.?$/.test(text)) {
letter = text;
} else if (text) {
content += text;
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
if (node.classList.contains('gxb-icon-radio') || node.classList.contains('gxb-icon-check')) {
// 忽略图标
continue;
}
// 有些选项可能把字母包在 span 里
const nodeText = node.innerText.trim();
if (/^[A-Z]\.?$/.test(nodeText) && !letter) {
letter = nodeText;
} else {
content += nodeText;
}
}
}
// 如果没提取到字母,尝试从整体文本中提取
if (!letter) {
const fullText = item.innerText.trim();
const match = fullText.match(/^([A-Z])\.?\s*/);
if (match) {
letter = match[1] + '.';
content = fullText.substring(match[0].length).trim();
} else {
content = fullText;
}
}
const fullText = letter ? (letter + ' ' + content).trim() : content;
if (fullText) options.push(fullText);
});
return options;
}
// ========== 选项选择 ==========
function selectOptionForAnswerWap(answerWap, answer) {
const letters = answer.match(/[A-D]/gi);
if (!letters) {
console.warn('⚠️ 未识别到有效答案字母:', answer);
return;
}
const answerItems = answerWap.querySelectorAll('.answer');
letters.forEach(letter => {
const targetIndex = letter.toUpperCase().charCodeAt(0) - 65;
const targetItem = answerItems[targetIndex];
if (!targetItem) return;
const icon = targetItem.querySelector('i');
if (!icon) return;
const isSelected = icon.classList.contains('gxb-icon-radio-selected') ||
icon.classList.contains('gxb-icon-check-selected') ||
icon.classList.contains('selected');
if (!isSelected) {
// 模拟完整点击事件,确保平台响应
['mousedown', 'mouseup', 'click'].forEach(eventType => {
icon.dispatchEvent(new MouseEvent(eventType, { bubbles: true, cancelable: true }));
});
console.log(`📌 已选择: ${letter}`);
} else {
console.log(`⏭️ 选项 ${letter} 已选中,跳过`);
}
});
}
// ========== AI 批量答题 ==========
async function answerAllQuestions() {
const apiKeyNow = getApiKey();
if (!apiKeyNow) {
alert('请先设置 API Key!');
showSettingsPanel();
return;
}
const questions = getAllQuestions();
if (!questions.length) {
alert('❌ 未检测到任何题目,请确保在考试/测验页面');
return;
}
console.log(`🎯 本页共检测到 ${questions.length} 道题目,开始逐题解答...`);
for (let i = 0; i < questions.length; i++) {
const q = questions[i];
const questionText = q.text;
const options = extractOptionsFromAnswerWap(q.answerElement);
if (!questionText || !options.length) {
console.warn(`⚠️ 第 ${i+1} 题无法提取题目或选项,跳过`);
continue;
}
console.log(`\n📌 第 ${i+1}/${questions.length} 题:${questionText.slice(0, 50)}...`);
const optionsText = options.join('\n');
const prompt = `请回答以下题目,只返回正确答案的字母(如 A, B, C 或 AB):\n题目:${questionText}\n选项:\n${optionsText}`;
const apiUrl = getApiUrl();
const model = getModel();
await new Promise(resolve => {
GM_xmlhttpRequest({
method: 'POST',
url: apiUrl,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKeyNow}`
},
data: JSON.stringify({
model: model,
messages: [
{ role: 'system', content: '你是一个考试答题助手,只返回正确答案的字母,不要任何解释。' },
{ role: 'user', content: prompt }
],
temperature: 0.1
}),
onload: function(resp) {
try {
const data = JSON.parse(resp.responseText);
const answer = data.choices[0].message.content.trim();
console.log(` AI 答案: ${answer}`);
selectOptionForAnswerWap(q.answerElement, answer);
} catch (e) {
console.error(' 解析 AI 响应失败:', e);
}
resolve();
},
onerror: function(err) {
console.error(' API 请求失败:', err);
resolve();
}
});
});
await new Promise(r => setTimeout(r, 1000));
}
console.log('🎉 本页所有题目处理完毕!');
}
// ========== 浮动控制面板 ==========
function addControlPanel() {
const panel = document.createElement('div');
panel.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:9998;background:#1fb6ff;color:white;padding:12px 18px;border-radius:12px;box-shadow:0 4px 15px rgba(0,0,0,0.2);display:flex;align-items:center;gap:12px;font-family:Arial,sans-serif;';
const status = document.createElement('span');
status.innerText = '🤖 AI 就绪';
status.style.fontWeight = 'bold';
const btn = document.createElement('button');
btn.innerText = '答本页全部';
btn.style.cssText = 'background:white;color:#1fb6ff;border:none;padding:6px 16px;border-radius:6px;cursor:pointer;font-weight:bold;font-size:14px;';
btn.onclick = () => {
status.innerText = '🤖 AI 答题中...';
answerAllQuestions().finally(() => {
status.innerText = '🤖 AI 就绪';
});
};
const settingsBtn = document.createElement('button');
settingsBtn.innerText = '⚙️';
settingsBtn.style.cssText = 'background:transparent;color:white;border:none;font-size:18px;cursor:pointer;padding:0 4px;';
settingsBtn.title = '打开设置';
settingsBtn.onclick = showSettingsPanel;
panel.appendChild(status);
panel.appendChild(btn);
panel.appendChild(settingsBtn);
document.body.appendChild(panel);
}
setTimeout(addControlPanel, 1500);
})();