// ==UserScript== // @name 重庆工程学院自动答题助手 v10.3 修复版 // @namespace http://tampermonkey.net/ // @version 10.3.0 // @description 修复题目文本提取,正确获取简答题内容 // @author wapokka // @match https://cqgcxy.wdjycj.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @connect dashscope.aliyuncs.com // @run-at document-end // ==/UserScript== (function() { 'use strict'; let CONFIG = { apiKey: GM_getValue('qwen_api_key', 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxx'), apiUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', model: GM_getValue('qwen_model', 'qwen-max'), autoAnswerInterval: 2000 }; let isRunning = false, isAnswering = false, answeredCount = 0, logs = []; const log = (msg) => { const time = new Date().toLocaleTimeString(); logs.push(`[${time}] ${msg}`); if (logs.length > 80) logs.shift(); console.log('[答题助手]', msg); const el = document.getElementById('kwai-logs'); if (el) el.innerHTML = logs.join('
'); }; const sleep = (ms) => new Promise(r => setTimeout(r, ms)); // ========== 核心提取逻辑 ========== const getQuestions = () => { const qs = []; // 找所有 strong 元素 const allStrongs = document.querySelectorAll('strong'); const questionPositions = []; allStrongs.forEach(s => { const m = s.textContent.match(/第\s*(\d+)\s*题/); if (m) { const rect = s.getBoundingClientRect(); questionPositions.push({ num: parseInt(m[1]), element: s, top: rect.top }); } }); questionPositions.sort((a, b) => a.top - b.top); // 找所有选项 (p 标签) const allParagraphs = document.querySelectorAll('p'); const optionPattern = /^([A-D])[、\.]\s*(.+)$/; const allOptions = []; allParagraphs.forEach(p => { const text = p.textContent.trim(); const m = text.match(optionPattern); if (m) { const rect = p.getBoundingClientRect(); allOptions.push({ label: m[1], text: m[2].trim(), element: p, top: rect.top }); } }); allOptions.sort((a, b) => a.top - b.top); // 找所有 radio const allRadios = document.querySelectorAll('input[type="radio"]'); const radioPositions = []; allRadios.forEach(r => { const rect = r.getBoundingClientRect(); radioPositions.push({ radio: r, top: rect.top }); }); radioPositions.sort((a, b) => a.top - b.top); // 找所有答题区 const allTextareas = document.querySelectorAll('textarea'); const allEditables = document.querySelectorAll('[contenteditable="true"]'); const essayInputs = []; allTextareas.forEach(ta => { const rect = ta.getBoundingClientRect(); essayInputs.push({ element: ta, type: 'textarea', top: rect.top }); }); allEditables.forEach(ed => { const rect = ed.getBoundingClientRect(); if (!essayInputs.find(e => e.element === ed)) { essayInputs.push({ element: ed, type: 'contenteditable', top: rect.top }); } }); essayInputs.sort((a, b) => a.top - b.top); // ========== 关键修复:提取题目文本 ========== const getQuestionText = (strongEl, qTop, qBottom) => { // 方法1: 找 strong 后面的所有兄弟节点 let questionText = ''; let node = strongEl.nextSibling; let attempts = 0; const maxAttempts = 20; while (node && attempts < maxAttempts) { attempts++; // 获取节点文本 let text = ''; if (node.nodeType === Node.TEXT_NODE) { text = node.textContent.trim(); } else if (node.nodeType === Node.ELEMENT_NODE) { // 跳过某些元素 const tagName = node.tagName.toLowerCase(); if (tagName === 'strong' || tagName === 'input' || tagName === 'button') { node = node.nextSibling; continue; } // 跳过选项段落 if (tagName === 'p' && /^[A-D][、\.]/.test(node.textContent.trim())) { break; } // 跳过 "答题区" 标记 if (tagName === 'em' || tagName === 'emphasis') { if (node.textContent.includes('答题区')) { break; } } text = node.textContent.trim(); } // 清理文本 text = text.replace(/收藏本题目/g, '').trim(); // 如果遇到选项或答题区,停止 if (/^[A-D][、\.]/.test(text) || text.includes('答题区') || text.includes('你的答案')) { break; } if (text) { questionText += ' ' + text; } node = node.nextSibling; } // 方法2: 如果方法1没找到,尝试从父元素查找 if (!questionText.trim()) { const parent = strongEl.parentElement; if (parent) { // 找 strong 后面的 p 标签 let foundStrong = false; for (const child of parent.children) { if (child === strongEl) { foundStrong = true; continue; } if (!foundStrong) continue; const tagName = child.tagName.toLowerCase(); const text = child.textContent.trim(); // 跳过选项 if (tagName === 'p' && /^[A-D][、\.]/.test(text)) break; // 跳过答题区 if (text.includes('答题区')) break; if (text && !text.match(/第\s*\d+\s*题/)) { questionText += ' ' + text.replace(/收藏本题目/g, '').trim(); } } } } // 方法3: 使用 DOM 范围查找 if (!questionText.trim()) { // 找 strong 的父元素下的所有文本 const parent = strongEl.parentElement; if (parent) { const fullText = parent.textContent.trim(); // 提取 strong 后面的内容 const strongText = strongEl.textContent.trim(); const idx = fullText.indexOf(strongText); if (idx >= 0) { let after = fullText.substring(idx + strongText.length); // 截取到 "收藏" 或选项之前 const stopMarkers = ['收藏', '答题区', '你的答案', 'A、', 'A.', 'A ']; for (const marker of stopMarkers) { const stopIdx = after.indexOf(marker); if (stopIdx > 0) { after = after.substring(0, stopIdx); } } questionText = after.trim(); } } } return questionText.trim(); }; // 组装题目 questionPositions.forEach((q, idx) => { const nextQ = questionPositions[idx + 1]; const qTop = q.top; const qBottom = nextQ ? nextQ.top : Infinity; // 找选项 const qOptions = allOptions.filter(opt => opt.top > qTop && opt.top < qBottom).slice(0, 4); // 找 radio const qRadios = radioPositions.filter(r => r.top > qTop && r.top < qBottom).map(r => r.radio); // 关联 radio 到选项 qOptions.forEach((opt, i) => { if (qRadios[i]) opt.radio = qRadios[i]; }); // 找简答输入区 const qEssayInputs = essayInputs.filter(e => e.top > qTop && e.top < qBottom); // 获取题目文本 - 使用新函数 const questionText = getQuestionText(q.element, qTop, qBottom); // 判断题目类型 let type = 'unknown'; if (qOptions.length >= 2) { type = 'choice'; } else if (qEssayInputs.length > 0) { type = 'essay'; } qs.push({ num: q.num, strong: q.element, questionText: questionText, options: qOptions, radios: qRadios, essayInputs: qEssayInputs, type: type }); }); return qs.sort((a, b) => a.num - b.num); }; // ========== 选择答案 ========== const selectAns = (letter, q) => { const opt = q.options.find(o => o.label === letter.toUpperCase()); if (!opt || !opt.radio) { log(`❌ 找不到选项 ${letter}`); return false; } const radio = opt.radio; log(`选择 ${letter}: ${opt.text.substring(0, 30)}...`); try { radio.checked = true; radio.click(); radio.dispatchEvent(new Event('change', { bubbles: true })); if (opt.element) { opt.element.style.cssText = 'background:#4caf50 !important;color:#fff !important;border-radius:4px !important;padding:4px 8px !important;'; } log(`✅ 已选择 ${letter}`); return true; } catch (e) { log(`❌ 点击失败: ${e.message}`); return false; } }; // ========== 填写简答 ========== const fillEssay = (text, q) => { if (!q.essayInputs || q.essayInputs.length === 0) { log(`❌ 找不到答题区`); return false; } const input = q.essayInputs[0]; const el = input.element; log(`填写简答: ${text.substring(0, 40)}...`); try { if (input.type === 'textarea') { el.value = text; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } else { el.focus(); el.innerHTML = text; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); el.blur(); } el.style.cssText = 'border:2px solid #4caf50!important;background:#f0fff0!important;'; log(`✅ 已填写简答`); return true; } catch (e) { log(`❌ 填写失败: ${e.message}`); return false; } }; // ========== 生成Prompt ========== const generatePrompt = (q) => { if (q.type === 'choice') { return `直接给出答案不要解释 题目:${q.questionText} 选项: ${q.options.map(o => `${o.label}. ${o.text}`).join('\n')} 请只回答一个字母(A/B/C/D),不要解释。`; } else if (q.type === 'essay') { return `请简要回答以下问题(不超过150字): ${q.questionText} 直接给出答案,不要解释,不要分段。`; } return q.questionText; }; // ========== 调用API ========== const callAPI = (q) => { return new Promise(resolve => { const prompt = generatePrompt(q); log(`========== 第${q.num}题 [${q.type}] ==========`); log(`题目: ${q.questionText.substring(0, 80)}`); if (q.type === 'choice') { q.options.forEach(o => log(` ${o.label}. ${o.text.substring(0, 35)}`)); } GM_xmlhttpRequest({ method: 'POST', url: CONFIG.apiUrl, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CONFIG.apiKey}` }, data: JSON.stringify({ model: CONFIG.model, messages: [{ role: 'user', content: prompt }], max_tokens: q.type === 'choice' ? 5 : 300, temperature: 0.05 }), timeout: 30000, onload: res => { if (res.status !== 200) { log(`API错误: ${res.status}`); resolve(null); return; } try { const content = JSON.parse(res.responseText).choices[0].message.content.trim(); log(`AI回答: ${content.substring(0, 60)}`); if (q.type === 'choice') { const m = content.match(/[A-D]/i); resolve(m ? m[0].toUpperCase() : null); } else { resolve(content); } } catch (e) { log(`解析错误: ${e.message}`); resolve(null); } }, onerror: () => { log('网络错误'); resolve(null); }, ontimeout: () => { log('请求超时'); resolve(null); } }); }); }; // ========== 答一道题 ========== const answerOne = async (q) => { const ans = await callAPI(q); if (!ans) { log(`❌ 第${q.num}题无答案`); return false; } let success = false; if (q.type === 'choice') { success = selectAns(ans, q); } else if (q.type === 'essay') { success = fillEssay(ans, q); } if (success) { answeredCount++; document.getElementById('kwai-answered').textContent = answeredCount; log(`✅ 第${q.num}题完成`); } return success; }; // ========== 答全部 ========== const answerAll = async () => { if (isAnswering) return; isAnswering = true; isRunning = true; answeredCount = 0; const qs = getQuestions(); log(`🚀 开始答题: ${qs.length}题, 模型: ${CONFIG.model}`); for (const q of qs) { if (!isRunning) break; await answerOne(q); await sleep(CONFIG.autoAnswerInterval); } isAnswering = false; log('✅ 全部完成'); }; const stop = () => { isRunning = false; isAnswering = false; log('⏹ 已停止'); }; // ========== 检测题目 ========== const detectQuestions = () => { const qs = getQuestions(); document.getElementById('kwai-total').textContent = qs.length; const choiceCount = qs.filter(q => q.type === 'choice').length; const essayCount = qs.filter(q => q.type === 'essay').length; log(`检测到 ${qs.length} 题 (选择${choiceCount}, 简答${essayCount})`); qs.forEach(q => { const preview = q.questionText.substring(0, 50); if (q.type === 'choice') { log(`第${q.num}题[选择]: ${preview}`); } else if (q.type === 'essay') { log(`第${q.num}题[简答]: ${preview}`); } else { log(`第${q.num}题[未知]: ${preview}`); } }); return qs; }; // ========== 设置面板 ========== const showSettings = () => { const old = document.getElementById('kwai-settings'); if (old) { old.remove(); return; } const div = document.createElement('div'); div.id = 'kwai-settings'; div.innerHTML = `
⚙️ 设置 ×
`; document.body.appendChild(div); document.getElementById('kwai-close-settings').onclick = () => div.remove(); document.getElementById('kwai-test-btn').onclick = () => { const key = document.getElementById('kwai-apikey').value.trim(); const model = document.getElementById('kwai-model').value; const res = document.getElementById('kwai-test-result'); res.style.display = 'block'; res.style.background = '#333'; res.textContent = '测试中...'; GM_xmlhttpRequest({ method: 'POST', url: CONFIG.apiUrl, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${key}` }, data: JSON.stringify({ model: model, messages: [{ role: 'user', content: '什么是单片机?请用一句话回答。' }], max_tokens: 50, temperature: 0.05 }), timeout: 15000, onload: r => { if (r.status === 200) { try { const ans = JSON.parse(r.responseText).choices[0].message.content.trim(); res.style.background = 'rgba(76,175,80,0.3)'; res.textContent = '测试成功: ' + ans.substring(0, 50); } catch (e) { res.style.background = 'rgba(244,67,54,0.3)'; res.textContent = '解析失败'; } } else { res.style.background = 'rgba(244,67,54,0.3)'; res.textContent = '错误 ' + r.status; } }, onerror: () => { res.style.background = 'rgba(244,67,54,0.3)'; res.textContent = '网络错误'; }, ontimeout: () => { res.style.background = 'rgba(244,67,54,0.3)'; res.textContent = '超时'; } }); }; document.getElementById('kwai-save-btn').onclick = () => { const key = document.getElementById('kwai-apikey').value.trim(); const model = document.getElementById('kwai-model').value; GM_setValue('qwen_api_key', key); GM_setValue('qwen_model', model); CONFIG.apiKey = key; CONFIG.model = model; div.remove(); log('✅ 设置已保存: ' + model); }; }; // ========== 批量面板 ========== const showBatch = () => { const old = document.getElementById('kwai-batch'); if (old) { old.remove(); return; } const qs = getQuestions(); const div = document.createElement('div'); div.id = 'kwai-batch'; div.innerHTML = `
📋 批量答题 (${qs.length}题) ×
范围:
`; document.body.appendChild(div); document.getElementById('kwai-close-batch').onclick = () => div.remove(); document.getElementById('batch-all').onclick = () => { div.remove(); answerAll(); }; document.getElementById('batch-start').onclick = () => { const s = parseInt(document.getElementById('batch-s').value) || 1; const e = parseInt(document.getElementById('batch-e').value) || qs.length; div.remove(); const target = qs.filter(q => q.num >= s && q.num <= e); log(`批量 ${s}-${e}, 共${target.length}题`); (async () => { isAnswering = true; isRunning = true; answeredCount = 0; for (const q of target) { if (!isRunning) break; await answerOne(q); await sleep(CONFIG.autoAnswerInterval); } isAnswering = false; log('完成'); })(); }; }; // ========== 拖拽 ========== const makeDraggable = (element, handle) => { let isDragging = false, startX, startY, startLeft, startTop; handle.style.cursor = 'move'; handle.addEventListener('mousedown', (e) => { isDragging = true; startX = e.clientX; startY = e.clientY; startLeft = element.offsetLeft; startTop = element.offsetTop; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; element.style.left = (startLeft + e.clientX - startX) + 'px'; element.style.top = (startTop + e.clientY - startY) + 'px'; element.style.right = 'auto'; }); document.addEventListener('mouseup', () => isDragging = false); }; // ========== 主面板 ========== const createPanel = () => { if (document.getElementById('kwai-panel')) return; const div = document.createElement('div'); div.id = 'kwai-panel'; div.innerHTML = `
🤖 答题助手 v10.3
⚙️ ×
0
已答
0
题目
点击检测开始
`; document.body.appendChild(div); makeDraggable(div, document.getElementById('kwai-header')); document.getElementById('btn-set').onclick = showSettings; document.getElementById('btn-close').onclick = () => div.style.display = 'none'; document.getElementById('btn-batch').onclick = showBatch; document.getElementById('btn-all').onclick = answerAll; document.getElementById('btn-check').onclick = detectQuestions; document.getElementById('btn-stop').onclick = stop; setTimeout(() => { detectQuestions(); log('✅ v10.3 已加载 - 修复题目提取'); }, 500); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createPanel); } else { createPanel(); } GM_registerMenuCommand('⚙️ 设置', showSettings); })();