// ==UserScript== // @name 厦门理工高校邦小测AI助手 // @namespace https://github.com/Wu557666/gaoxiaobangai // @version 2.1.3 // @description 支持超时控制、自动重试、实时进度显示,极速并发答题(修复API错误处理) // @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 $ = window.$; const wait = ms => new Promise(r => setTimeout(r, ms)); // ========== 默认配置 ========== const DEFAULT_API_URL = 'https://api.deepseek.com/chat/completions'; const DEFAULT_MODEL = 'deepseek-chat'; const DEFAULT_TIMEOUT = 30000; // 30秒,避免正常请求超时 const DEFAULT_RETRY = 2; // 失败重试2次 const DEFAULT_INTERVAL = 300; // 题目间延迟 const STORAGE_API_KEY = 'deepseek_api_key'; const STORAGE_API_URL = 'deepseek_api_url'; const STORAGE_MODEL = 'deepseek_model'; const STORAGE_INTERVAL = 'deepseek_interval'; const STORAGE_TIMEOUT = 'deepseek_timeout'; const STORAGE_RETRY = 'deepseek_retry'; // ========== 配置读写 ========== 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 getInterval() { return parseInt(GM_getValue(STORAGE_INTERVAL, DEFAULT_INTERVAL), 10); } function setIntervalMs(ms) { GM_setValue(STORAGE_INTERVAL, ms); } function getTimeout() { return parseInt(GM_getValue(STORAGE_TIMEOUT, DEFAULT_TIMEOUT), 10); } function setTimeoutMs(ms) { GM_setValue(STORAGE_TIMEOUT, ms); } function getRetry() { return parseInt(GM_getValue(STORAGE_RETRY, DEFAULT_RETRY), 10); } function setRetry(count) { GM_setValue(STORAGE_RETRY, count); } // ========== 设置面板 ========== function showSettingsPanel() { const oldOverlay = document.getElementById('ai-settings-overlay'); if (oldOverlay) oldOverlay.remove(); const apiKey = getApiKey(); const apiUrl = getApiUrl(); const model = getModel(); const interval = getInterval(); const timeout = getTimeout(); const retry = getRetry(); 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:420px;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(); const newTimeout = document.getElementById('ai-timeout-input').value.trim(); const newRetry = document.getElementById('ai-retry-input').value.trim(); const newInterval = document.getElementById('ai-interval-input').value.trim(); if (!newKey) { alert('❌ API Key 不能为空!'); return; } setApiKey(newKey); if (newUrl) setApiUrl(newUrl); if (newModel) setModel(newModel); if (newTimeout) setTimeoutMs(newTimeout); if (newRetry) setRetry(newRetry); if (newInterval) setIntervalMs(newInterval); overlay.remove(); alert('✅ 配置已保存!现在可以使用 AI 答题了。'); location.reload(); }; } GM_registerMenuCommand('⚙️ 打开设置面板', showSettingsPanel); GM_registerMenuCommand('📋 查看当前配置', () => { const key = getApiKey(); const url = getApiUrl(); const model = getModel(); const timeout = getTimeout(); const retry = getRetry(); const interval = getInterval(); const keyPreview = key ? key.slice(0, 8) + '...' + key.slice(-4) : '未设置'; alert(`当前配置:\n\nAPI Key: ${keyPreview}\nAPI 地址: ${url}\n模型: ${model}\n超时: ${timeout}ms\n重试: ${retry}次\n间隔: ${interval}ms`); }); const apiKey = getApiKey(); if (!apiKey) { console.warn('⚠️ 未设置 DeepSeek API Key,正在打开设置面板...'); setTimeout(showSettingsPanel, 1000); return; } console.log('✅ DeepSeek 配置已加载'); // ========== 从全局变量读取题目数据 ========== function getQuestionsFromData() { return window.questionList || (typeof unsafeWindow !== 'undefined' && unsafeWindow.questionList); } // ========== 根据 answer_id 精准点击选项 ========== function selectOptionByAnswerId(answerId) { if (!answerId) return false; const icon = document.querySelector(`i[answer_id="${answerId}"]`); if (!icon) return false; const isSelected = icon.classList.contains('gxb-icon-radio-selected') || icon.classList.contains('gxb-icon-check-selected') || icon.classList.contains('selected'); if (isSelected) { return true; } ['mousedown', 'mouseup', 'click'].forEach(type => { icon.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true })); }); return true; } // ========== 带状态检查的重试请求 ========== function requestWithRetry(url, headers, data, timeoutMs, maxRetry) { return new Promise((resolve, reject) => { let attempts = 0; const doRequest = () => { GM_xmlhttpRequest({ method: 'POST', url: url, headers: headers, data: data, timeout: timeoutMs, onload: function(resp) { // 检查HTTP状态码 if (resp.status < 200 || resp.status >= 300) { console.error(`API 请求失败: HTTP ${resp.status}`, resp.responseText?.slice(0, 200)); if (attempts < maxRetry) { attempts++; console.warn(`重试 (${attempts}/${maxRetry})...`); setTimeout(doRequest, 1500); } else { reject(new Error(`HTTP ${resp.status}`)); } return; } // 解析JSON并验证结构 let data; try { data = JSON.parse(resp.responseText); } catch (e) { console.error('JSON解析失败', resp.responseText?.slice(0, 200)); if (attempts < maxRetry) { attempts++; setTimeout(doRequest, 1500); } else { reject(new Error('JSON parse error')); } return; } if (!data.choices || !Array.isArray(data.choices) || data.choices.length === 0) { console.error('API返回数据异常(无choices)', data); if (attempts < maxRetry) { attempts++; setTimeout(doRequest, 1500); } else { reject(new Error('No choices in response')); } return; } resolve(resp); }, onerror: function(err) { console.error('网络请求错误', err); if (attempts < maxRetry) { attempts++; console.warn(`重试 (${attempts}/${maxRetry})...`); setTimeout(doRequest, 1500); } else { reject(err); } }, ontimeout: function() { console.error('请求超时'); if (attempts < maxRetry) { attempts++; console.warn(`重试 (${attempts}/${maxRetry})...`); setTimeout(doRequest, 1500); } else { reject(new Error('Timeout')); } } }); }; doRequest(); }); } // ========== 并发批量答题 ========== async function answerAllQuestions(statusElement) { const apiKeyNow = getApiKey(); if (!apiKeyNow) { alert('请先设置 API Key!'); showSettingsPanel(); return; } const questionData = getQuestionsFromData(); if (!questionData || questionData.length === 0) { alert('❌ 未获取到题目数据,请刷新后重试。'); return; } const apiUrl = getApiUrl(); const model = getModel(); const timeout = getTimeout(); const maxRetry = getRetry(); const interval = getInterval(); const total = questionData.length; let completed = 0; const updateStatus = () => { if (statusElement) statusElement.innerText = `🤖 答题中 ${completed}/${total}`; }; updateStatus(); console.log(`🎯 共 ${total} 道题目,开始并发解答(超时:${timeout}ms, 重试:${maxRetry}次)...`); const promises = questionData.map(async (q, index) => { const questionText = q.name || q.questionName; const options = q.answerList || []; if (!questionText || options.length === 0) { console.warn(`⚠️ 第 ${index+1} 题数据不完整,跳过`); completed++; updateStatus(); return; } const optionsText = options.map((opt, i) => `${String.fromCharCode(65 + i)}. ${opt.text || opt}` ).join('\n'); const prompt = `请回答以下题目,只返回正确答案的字母(如 A, B, C 或 AB):\n题目:${questionText}\n选项:\n${optionsText}`; try { const resp = await requestWithRetry( apiUrl, { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKeyNow}` }, JSON.stringify({ model: model, messages: [ { role: 'system', content: '你是一个考试答题助手,只返回正确答案的字母,不要任何解释。' }, { role: 'user', content: prompt } ], temperature: 0.1 }), timeout, maxRetry ); const data = JSON.parse(resp.responseText); const answer = data.choices[0].message.content.trim(); console.log(`📌 第 ${index+1} 题 AI 答案: ${answer}`); const letters = answer.match(/[A-D]/gi); if (letters) { letters.forEach(letter => { const optIndex = letter.toUpperCase().charCodeAt(0) - 65; if (optIndex < options.length) { const opt = options[optIndex]; if (opt.answerId) { selectOptionByAnswerId(opt.answerId); } } }); } else { console.warn(` ⚠️ 未识别到有效答案字母: ${answer}`); } } catch (e) { console.error(`❌ 第 ${index+1} 题最终失败:`, e.message || e); } finally { completed++; updateStatus(); if (interval > 0) await wait(interval); } }); await Promise.all(promises); if (statusElement) statusElement.innerText = '🤖 AI 就绪'; 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 = () => answerAllQuestions(status); 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); } function waitForQuestionData() { return new Promise(resolve => { const check = () => { const q = getQuestionsFromData(); if (q && q.length > 0) resolve(); else setTimeout(check, 500); }; check(); }); } (async function init() { await waitForQuestionData(); addControlPanel(); })(); })();