// ==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 = `