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