// ==UserScript==
// @name 小rain的习传助手
// @namespace http://tampermonkey.net/
// @version 3.0
// @description 习传网智能答题助手 - 支持多模型API、批量答题、自动跳转
// @author Rain
// @match *://*.xichuanwh.com/*
// @match *://xichuanwh.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @connect *
// ==/UserScript==
(function() {
'use strict';
// ==================== 多模型配置 ====================
const MODEL_CONFIGS = {
deepseek: {
name: 'DeepSeek',
url: 'https://api.deepseek.com/v1/chat/completions',
model: 'deepseek-chat',
keyPlaceholder: 'sk-...'
},
openai: {
name: 'OpenAI',
url: 'https://api.openai.com/v1/chat/completions',
model: 'gpt-3.5-turbo',
keyPlaceholder: 'sk-...'
},
claude: {
name: 'Claude',
url: 'https://api.anthropic.com/v1/messages',
model: 'claude-3-haiku-20240307',
keyPlaceholder: 'sk-ant-...'
},
kimi: {
name: 'Kimi',
url: 'https://api.moonshot.cn/v1/chat/completions',
model: 'moonshot-v1-8k',
keyPlaceholder: 'sk-...'
},
qwen: {
name: '通义千问',
url: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation',
model: 'qwen-turbo',
keyPlaceholder: 'sk-...'
},
custom: {
name: '自定义',
url: '',
model: '',
keyPlaceholder: '输入API Key'
}
};
// ==================== 状态 ====================
let currentModel = GM_getValue('rain_model', 'deepseek');
let apiKey = GM_getValue('rain_api_key', '');
let customUrl = GM_getValue('rain_custom_url', '');
let customModel = GM_getValue('rain_custom_model', '');
let answers = [];
let isRunning = false;
let isPanelCollapsed = false;
// 批量答题状态
let batchMode = false;
let questionList = []; // 题目列表
let currentQuestionIndex = 0; // 当前题目索引
let autoNext = false; // 是否自动跳转下一题
let skipCompleted = false; // 是否跳过已做题目
// 自动刷题状态
let autoSolving = false; // 是否正在自动刷题
let autoSolveQueue = []; // 自动刷题队列
let autoSolveIndex = 0; // 当前自动刷题索引
// ==================== UI样式 ====================
const STYLES = `
#rain-xichuan-container {
position: fixed;
top: 10px;
right: 10px;
z-index: 99999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
#rain-xichuan-panel {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 2px;
box-shadow: 0 10px 40px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.1);
min-width: 320px;
max-width: 400px;
transition: all 0.3s ease;
resize: both;
overflow: hidden;
}
#rain-xichuan-panel.collapsed {
min-width: 150px;
resize: none;
}
#rain-xichuan-panel .panel-inner {
background: rgba(255,255,255,0.98);
border-radius: 14px;
overflow: hidden;
}
#rain-xichuan-panel .panel-header {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px 16px;
cursor: move;
display: flex;
align-items: center;
justify-content: space-between;
user-select: none;
}
#rain-xichuan-panel .panel-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
#rain-xichuan-panel .panel-header .header-actions {
display: flex;
gap: 8px;
}
#rain-xichuan-panel .panel-header button {
background: rgba(255,255,255,0.2);
border: none;
color: white;
width: 28px;
height: 28px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
#rain-xichuan-panel .panel-header button:hover {
background: rgba(255,255,255,0.3);
}
#rain-xichuan-panel .panel-body {
padding: 16px;
}
#rain-xichuan-panel.collapsed .panel-body {
display: none;
}
#rain-xichuan-panel .section {
margin-bottom: 16px;
}
#rain-xichuan-panel .section-title {
font-size: 12px;
font-weight: 600;
color: #666;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
#rain-xichuan-panel select, #rain-xichuan-panel input[type="text"], #rain-xichuan-panel input[type="password"] {
width: 100%;
padding: 10px 12px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 13px;
box-sizing: border-box;
transition: all 0.2s;
background: #fafafa;
}
#rain-xichuan-panel select:focus, #rain-xichuan-panel input:focus {
outline: none;
border-color: #667eea;
background: white;
}
#rain-xichuan-panel .btn {
width: 100%;
padding: 12px;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-bottom: 10px;
}
#rain-xichuan-panel .btn-primary {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
color: white;
}
#rain-xichuan-panel .btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
#rain-xichuan-panel .btn-secondary {
background: #f0f0f0;
color: #333;
}
#rain-xichuan-panel .btn-secondary:hover {
background: #e0e0e0;
}
#rain-xichuan-panel .btn-success {
background: linear-gradient(90deg, #11998e 0%, #38ef7d 100%);
color: white;
}
#rain-xichuan-panel .btn-success:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(17, 153, 142, 0.4);
}
#rain-xichuan-panel .btn-warning {
background: linear-gradient(90deg, #f093fb 0%, #f5576c 100%);
color: white;
}
#rain-xichuan-panel .status {
padding: 10px 12px;
background: #f8f9fa;
border-radius: 8px;
font-size: 13px;
color: #666;
margin-bottom: 12px;
border-left: 3px solid #667eea;
}
#rain-xichuan-panel .status.error {
border-left-color: #f5576c;
background: #fff5f5;
color: #c53030;
}
#rain-xichuan-panel .status.success {
border-left-color: #38ef7d;
background: #f0fff4;
color: #276749;
}
#rain-xichuan-panel .answers-container {
background: #f8f9fa;
border-radius: 10px;
padding: 12px;
max-height: 200px;
overflow-y: auto;
font-size: 13px;
}
#rain-xichuan-panel .answer-item {
display: flex;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid #e0e0e0;
}
#rain-xichuan-panel .answer-item:last-child {
border-bottom: none;
}
#rain-xichuan-panel .answer-num {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
color: white;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
margin-right: 10px;
}
#rain-xichuan-panel .answer-val {
font-weight: 600;
color: #333;
}
#rain-xichuan-panel .hint-box {
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
border-radius: 10px;
padding: 12px;
margin-bottom: 12px;
font-size: 12px;
color: #744210;
}
#rain-xichuan-panel .hint-box strong {
display: block;
margin-bottom: 4px;
font-size: 13px;
}
#rain-xichuan-panel .custom-fields {
display: none;
}
#rain-xichuan-panel .custom-fields.visible {
display: block;
}
#rain-xichuan-panel .resize-handle {
position: absolute;
bottom: 0;
right: 0;
width: 20px;
height: 20px;
cursor: se-resize;
background: linear-gradient(135deg, transparent 50%, rgba(102, 126, 234, 0.3) 50%);
border-radius: 0 0 14px 0;
}
#rain-xichuan-panel .author-info {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e0e0e0;
text-align: center;
font-size: 11px;
color: #999;
}
#rain-xichuan-panel .author-info a {
color: #667eea;
text-decoration: none;
}
#rain-xichuan-panel .author-info a:hover {
text-decoration: underline;
}
/* 批量答题样式 */
#rain-xichuan-panel .batch-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
padding: 12px;
margin-bottom: 12px;
color: white;
}
#rain-xichuan-panel .batch-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
}
#rain-xichuan-panel .batch-progress {
font-size: 12px;
margin-bottom: 8px;
opacity: 0.9;
}
#rain-xichuan-panel .batch-list {
max-height: 150px;
overflow-y: auto;
background: rgba(255,255,255,0.1);
border-radius: 6px;
padding: 8px;
font-size: 11px;
}
#rain-xichuan-panel .batch-item {
padding: 4px 0;
border-bottom: 1px solid rgba(255,255,255,0.2);
display: flex;
justify-content: space-between;
align-items: center;
}
#rain-xichuan-panel .batch-item:last-child {
border-bottom: none;
}
#rain-xichuan-panel .batch-item.completed {
opacity: 0.6;
text-decoration: line-through;
}
#rain-xichuan-panel .batch-item.current {
font-weight: bold;
color: #ffecd2;
}
#rain-xichuan-panel .batch-controls {
display: flex;
gap: 8px;
margin-top: 8px;
}
/* 自动刷题样式 */
#rain-xichuan-panel #rain-auto-solve-section {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
border-radius: 10px;
padding: 12px;
margin-bottom: 12px;
color: white;
}
#rain-xichuan-panel #rain-auto-solve-section .batch-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
}
#rain-xichuan-panel #rain-auto-solve-list {
background: rgba(255,255,255,0.15);
border-radius: 6px;
padding: 8px;
}
#rain-xichuan-panel #rain-auto-solve-list > div {
padding: 6px;
border-bottom: 1px solid rgba(255,255,255,0.2);
transition: background 0.2s;
}
#rain-xichuan-panel #rain-auto-solve-list > div:hover {
background: rgba(255,255,255,0.1);
}
#rain-xichuan-panel #rain-auto-solve-list > div:last-child {
border-bottom: none;
}
#rain-xichuan-panel #rain-auto-solve-list input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
}
#rain-xichuan-panel .batch-controls .btn {
flex: 1;
margin-bottom: 0;
padding: 8px;
font-size: 12px;
}
#rain-xichuan-panel .auto-next-toggle {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
margin-top: 8px;
}
#rain-xichuan-panel .auto-next-toggle input[type="checkbox"] {
width: auto;
}
#rain-about-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 100000;
display: flex;
align-items: center;
justify-content: center;
}
#rain-about-modal .modal-content {
background: white;
border-radius: 16px;
padding: 24px;
max-width: 360px;
text-align: center;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
#rain-about-modal .modal-content h2 {
margin: 0 0 16px 0;
color: #667eea;
}
#rain-about-modal .modal-content .author {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
#rain-about-modal .modal-content .school {
font-size: 13px;
color: #999;
margin-bottom: 16px;
}
#rain-about-modal .modal-content .desc {
font-size: 13px;
color: #555;
line-height: 1.6;
margin-bottom: 20px;
}
#rain-about-modal .modal-content button {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 10px 32px;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
}
`;
// ==================== UI ====================
function createPanel() {
// 添加样式
const styleEl = document.createElement('style');
styleEl.textContent = STYLES;
document.head.appendChild(styleEl);
// 创建容器
const container = document.createElement('div');
container.id = 'rain-xichuan-container';
const panel = document.createElement('div');
panel.id = 'rain-xichuan-panel';
const config = MODEL_CONFIGS[currentModel];
panel.innerHTML = `
💡 使用提示
如果觉得好用可以分享给你的朋友,让他们进群支持我~
📋 批量答题模式
进度: 0/0
选择模型
💡 可直接点击"开始答题"手动刷题,或先"读取题目列表"批量刷题
🤖 自动刷题模式
已选: 0 题
就绪 - 请选择模型并配置API Key
`;
container.appendChild(panel);
document.body.appendChild(container);
// 绑定事件
bindEvents(panel);
// 初始化拖动功能
initDrag(panel);
// 如果有已保存的 API Key,隐藏配置区域
if (apiKey) {
document.getElementById('rain-api-config-section').style.display = 'none';
document.getElementById('rain-toggle-api-btn').style.display = 'flex';
updateStatus('已加载保存的配置,点击开始答题');
}
}
function bindEvents(panel) {
// 模型选择
const modelSelect = document.getElementById('rain-model-select');
modelSelect.onchange = () => {
currentModel = modelSelect.value;
const config = MODEL_CONFIGS[currentModel];
document.getElementById('rain-api-key').placeholder = config.keyPlaceholder;
const customFields = document.querySelector('.custom-fields');
if (currentModel === 'custom') {
customFields.classList.add('visible');
} else {
customFields.classList.remove('visible');
}
};
// 保存配置
document.getElementById('rain-save-btn').onclick = () => {
const key = document.getElementById('rain-api-key').value.trim();
const url = document.getElementById('rain-custom-url')?.value.trim() || '';
const model = document.getElementById('rain-custom-model')?.value.trim() || '';
if (key) {
GM_setValue('rain_api_key', key);
GM_setValue('rain_model', currentModel);
GM_setValue('rain_custom_url', url);
GM_setValue('rain_custom_model', model);
apiKey = key;
customUrl = url;
customModel = model;
updateStatus('配置已保存', 'success');
// 隐藏 API 配置区域,显示切换按钮
document.getElementById('rain-api-config-section').style.display = 'none';
document.getElementById('rain-toggle-api-btn').style.display = 'flex';
} else {
updateStatus('请输入 API Key', 'error');
}
};
// 切换 API 配置显示/隐藏
document.getElementById('rain-toggle-api-btn').onclick = () => {
const apiSection = document.getElementById('rain-api-config-section');
const toggleBtn = document.getElementById('rain-toggle-api-btn');
if (apiSection.style.display === 'none') {
apiSection.style.display = 'block';
toggleBtn.innerHTML = '🔼 隐藏 API 配置';
} else {
apiSection.style.display = 'none';
toggleBtn.innerHTML = '⚙️ 切换 API 配置';
}
};
// 读取题目列表
document.getElementById('rain-read-list-btn').onclick = readQuestionList;
// 自动刷题按钮
document.getElementById('rain-auto-solve-btn').onclick = showAutoSolveSelector;
// 开始/停止自动刷题
document.getElementById('rain-start-auto-btn').onclick = startAutoSolve;
document.getElementById('rain-stop-auto-btn').onclick = stopAutoSolve;
// 开始答题
document.getElementById('rain-start-btn').onclick = startAnswering;
// 填写/标记答案
document.getElementById('rain-fill-btn').onclick = fillAnswers;
// 提交答案
document.getElementById('rain-submit-btn').onclick = submitAnswers;
// 批量答题控制
document.getElementById('rain-prev-btn').onclick = goToPrevQuestion;
document.getElementById('rain-next-btn').onclick = goToNextQuestion;
document.getElementById('rain-auto-next').onchange = (e) => {
autoNext = e.target.checked;
GM_setValue('rain_auto_next', autoNext);
};
document.getElementById('rain-skip-completed').onchange = (e) => {
skipCompleted = e.target.checked;
GM_setValue('rain_skip_completed', skipCompleted);
};
// 加载保存的设置
autoNext = GM_getValue('rain_auto_next', false);
skipCompleted = GM_getValue('rain_skip_completed', false);
document.getElementById('rain-auto-next').checked = autoNext;
document.getElementById('rain-skip-completed').checked = skipCompleted;
// 收起/展开
document.getElementById('rain-collapse-btn').onclick = () => {
isPanelCollapsed = !isPanelCollapsed;
panel.classList.toggle('collapsed', isPanelCollapsed);
document.getElementById('rain-collapse-btn').textContent = isPanelCollapsed ? '+' : '−';
};
// 关闭面板
document.getElementById('rain-close-btn').onclick = () => {
if (confirm('确定要关闭助手面板吗?刷新页面可重新打开。')) {
document.getElementById('rain-xichuan-container').remove();
}
};
}
// ==================== 拖动功能 ====================
function initDrag(panel) {
const header = panel.querySelector('.panel-header');
const container = document.getElementById('rain-xichuan-container');
let isDragging = false;
let startX, startY, startLeft, startTop;
header.onmousedown = (e) => {
// 如果点击的是按钮,不触发拖动
if (e.target.tagName === 'BUTTON') return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startLeft = container.offsetLeft;
startTop = container.offsetTop;
panel.style.transition = 'none';
document.body.style.userSelect = 'none';
};
document.onmousemove = (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
container.style.left = (startLeft + dx) + 'px';
container.style.top = (startTop + dy) + 'px';
container.style.right = 'auto';
};
document.onmouseup = () => {
if (isDragging) {
isDragging = false;
panel.style.transition = '';
document.body.style.userSelect = '';
}
};
}
function updateStatus(text, type = '') {
const el = document.getElementById('rain-status');
if (el) {
el.textContent = text;
el.className = 'status ' + type;
}
console.log('[Rain习传]', text);
}
// ==================== 自动刷题功能 ====================
// 显示自动刷题选择界面
function showAutoSolveSelector() {
const section = document.getElementById('rain-auto-solve-section');
const list = document.getElementById('rain-auto-solve-list');
const autoSolveBtn = document.getElementById('rain-auto-solve-btn');
if (!section || !list) return;
// 隐藏自动刷题按钮,显示选择区域
if (autoSolveBtn) autoSolveBtn.style.display = 'none';
section.style.display = 'block';
// 生成题目列表(只显示未完成的题目)
list.innerHTML = '';
const unanswered = questionList.filter(q => !q.isCompleted);
if (unanswered.length === 0) {
list.innerHTML = '没有未完成的题目
';
return;
}
unanswered.forEach((q, idx) => {
const item = document.createElement('div');
item.className = 'batch-item';
item.style.cssText = 'display: flex; align-items: center; padding: 6px; border-bottom: 1px solid #eee; font-size: 13px;';
item.innerHTML = `
${idx + 1}. ${q.title}
`;
list.appendChild(item);
});
// 绑定复选框事件
const checkboxes = list.querySelectorAll('.rain-solve-checkbox');
checkboxes.forEach(cb => {
cb.addEventListener('change', updateSelectedCount);
});
// 全选复选框事件
const selectAll = document.getElementById('rain-select-all');
if (selectAll) {
selectAll.checked = true;
selectAll.addEventListener('change', (e) => {
checkboxes.forEach(cb => cb.checked = e.target.checked);
updateSelectedCount();
});
}
updateSelectedCount();
updateStatus(`请选择要自动刷的题目(共 ${unanswered.length} 道未做)`, 'success');
}
// 更新已选题目数量
function updateSelectedCount() {
const checked = document.querySelectorAll('.rain-solve-checkbox:checked');
const countEl = document.getElementById('rain-selected-count');
if (countEl) countEl.textContent = checked.length;
}
// 开始自动刷题
async function startAutoSolve() {
const checked = document.querySelectorAll('.rain-solve-checkbox:checked');
if (checked.length === 0) {
updateStatus('请至少选择一道题目', 'error');
return;
}
// 构建队列
autoSolveQueue = Array.from(checked).map(cb => parseInt(cb.dataset.index));
autoSolveIndex = 0;
autoSolving = true;
// 更新UI
document.getElementById('rain-start-auto-btn').style.display = 'none';
document.getElementById('rain-stop-auto-btn').style.display = 'block';
document.getElementById('rain-auto-solve-list').style.opacity = '0.6';
document.getElementById('rain-select-all').disabled = true;
document.querySelectorAll('.rain-solve-checkbox').forEach(cb => cb.disabled = true);
updateStatus(`开始自动刷题,共 ${autoSolveQueue.length} 道`, 'success');
// 开始刷第一题
await processAutoSolve();
}
// 处理自动刷题流程
async function processAutoSolve() {
if (!autoSolving || autoSolveIndex >= autoSolveQueue.length) {
finishAutoSolve();
return;
}
const questionIdx = autoSolveQueue[autoSolveIndex];
const question = questionList.find(q => q.index === questionIdx);
if (!question) {
autoSolveIndex++;
await processAutoSolve();
return;
}
updateStatus(`[${autoSolveIndex + 1}/${autoSolveQueue.length}] 正在处理: ${question.title.substring(0, 20)}...`, 'success');
// 保存当前进度
GM_setValue('rain_autosolve_queue', autoSolveQueue);
GM_setValue('rain_autosolve_index', autoSolveIndex);
GM_setValue('rain_autosolving', true);
// 进入题目
if (question.element) {
batchMode = true;
currentQuestionIndex = questionList.indexOf(question);
GM_setValue('rain_current_index', currentQuestionIndex);
GM_setValue('rain_batch_mode', true);
// 触发点击
question.element.click();
updateStatus(`[${autoSolveIndex + 1}/${autoSolveQueue.length}] 已进入题目,等待加载...`, 'success');
// 等待页面加载,然后自动答题
setTimeout(() => {
autoAnswerAndSubmit();
}, 2500);
} else {
autoSolveIndex++;
await processAutoSolve();
}
}
// 自动获取答案并提交
async function autoAnswerAndSubmit() {
if (!autoSolving) return;
updateStatus(`[${autoSolveIndex + 1}/${autoSolveQueue.length}] 正在获取答案...`, 'success');
try {
// 1. 提取页面内容
const { count, type } = getQuestionCount();
const pageText = extractPageText();
if (count === 0) {
updateStatus('未能提取题目内容,跳过', 'error');
setTimeout(() => nextAutoSolve(), 2000);
return;
}
// 2. 构建prompt并调用AI
const prompt = buildPrompt(pageText, count, type);
const aiResponse = await callAI(prompt);
const answers = parseAnswers(aiResponse, count);
if (!answers || answers.length === 0) {
updateStatus('未能获取答案,跳过', 'error');
setTimeout(() => nextAutoSolve(), 2000);
return;
}
// 3. 保存答案并填入
updateStatus(`[${autoSolveIndex + 1}/${autoSolveQueue.length}] 正在填入答案...`, 'success');
window.rainAnswers = answers;
fillAnswers();
// 4. 等待一下再提交
setTimeout(() => {
if (!autoSolving) return;
updateStatus(`[${autoSolveIndex + 1}/${autoSolveQueue.length}] 正在提交...`, 'success');
// 使用 submitAnswers 函数提交,它有更完善的查找逻辑
submitAnswers();
// 等待提交完成,然后处理可能的确认弹窗
setTimeout(() => {
// 查找并点击确认/确定按钮(提交后的提示弹窗)
let confirmBtn = document.querySelector('.el-message-box__btns .el-button--primary');
if (!confirmBtn) {
confirmBtn = document.querySelector('.el-message-box__btns button');
}
if (!confirmBtn) {
// 查找包含"确定"或"确认"文字的按钮
const allBtns = document.querySelectorAll('button');
for (const btn of allBtns) {
const text = btn.innerText?.trim() || '';
if (text === '确定' || text === '确认') {
confirmBtn = btn;
break;
}
}
}
if (confirmBtn) {
console.log('[DEBUG] 找到确认按钮,点击:', confirmBtn.innerText);
confirmBtn.click();
}
// 再等待一下后进入下一题
setTimeout(() => {
nextAutoSolve();
}, 1500);
}, 2500);
}, 2000);
} catch (err) {
console.error('自动答题出错:', err);
updateStatus('自动答题出错: ' + err.message, 'error');
setTimeout(() => nextAutoSolve(), 2000);
}
}
// 记录已完成的题目标题(避免重复做)
const completedTitles = new Set();
// 下一题
function nextAutoSolve() {
autoSolveIndex++;
if (autoSolveIndex >= autoSolveQueue.length) {
finishAutoSolve();
return;
}
// 返回列表页 - 尝试多种选择器
let backBtn = document.querySelector('.backBtn, .back-btn, .el-button--default, [class*="back"]');
// 如果没找到,尝试通过其他方式
if (!backBtn) {
// 查找包含返回图标的按钮(通常是第一个按钮)
const iconBtns = document.querySelectorAll('button i.el-icon-back, button i[class*="back"]');
if (iconBtns.length > 0) {
backBtn = iconBtns[0].closest('button');
console.log('[DEBUG] 通过返回图标找到按钮');
}
}
// 还是没找到,遍历所有按钮
if (!backBtn) {
const allBtns = Array.from(document.querySelectorAll('button'));
console.log('[DEBUG] 查找返回按钮,总按钮数:', allBtns.length);
// 打印前5个按钮的信息用于调试
allBtns.slice(0, 5).forEach((btn, i) => {
console.log(`[DEBUG] 按钮${i}:`, btn.innerText?.trim(), 'class:', btn.className);
});
for (const btn of allBtns) {
const text = (btn.innerText?.trim() || '').toLowerCase();
const className = (btn.className || '').toLowerCase();
// 匹配包含"返回"文字的按钮
if (text.includes('返回')) {
backBtn = btn;
console.log('[DEBUG] 通过文字找到返回按钮:', text);
break;
}
// 匹配class包含back的按钮
if (className.includes('back')) {
backBtn = btn;
console.log('[DEBUG] 通过class找到返回按钮:', className);
break;
}
}
}
if (backBtn) {
updateStatus(`[${autoSolveIndex + 1}/${autoSolveQueue.length}] 返回列表页...`, 'success');
console.log('[DEBUG] 找到返回按钮:', backBtn.innerText?.trim());
// 触发点击
backBtn.click();
// 等待2秒后才开始轮询,给页面切换时间
setTimeout(() => {
// 等待页面返回列表,轮询检查表格是否出现
let attempts = 0;
const maxAttempts = 30; // 最多等待15秒
const waitForList = setInterval(() => {
attempts++;
const rows = document.querySelectorAll('.el-table__row');
console.log(`[DEBUG] 等待列表页... 尝试 ${attempts}, 表格行数: ${rows.length}`);
if (rows.length > 0) {
// 表格已出现,停止轮询
clearInterval(waitForList);
console.log('[DEBUG] 列表页已加载');
// 读取列表
readQuestionList();
// 等待一下确保列表读取完成
setTimeout(() => {
// 找到第一个未完成的题目(按钮文字是"做题"且不在completedTitles中)
let foundIndex = -1;
for (let i = 0; i < questionList.length; i++) {
const q = questionList[i];
// 通过按钮文字判断:"做题"表示未完成,"查看"表示已完成
// 同时排除已经记录为完成的题目
if (q.btnText && q.btnText.includes('做题') && !completedTitles.has(q.title)) {
foundIndex = i;
break;
}
}
if (foundIndex !== -1 && questionList[foundIndex]) {
console.log('[DEBUG] 找到下一题:', questionList[foundIndex].title, '索引:', foundIndex);
enterQuestion(foundIndex);
} else {
// 如果没有找到,可能是列表还没刷新,尝试找下一个在队列中的题目
console.log('[DEBUG] 未找到新的未做题,尝试从队列中找...');
const nextIdx = autoSolveQueue[autoSolveIndex];
if (nextIdx !== undefined && questionList[nextIdx] && !completedTitles.has(questionList[nextIdx].title)) {
console.log('[DEBUG] 从队列中找到:', questionList[nextIdx].title);
enterQuestion(nextIdx);
} else {
updateStatus('未找到未完成的题目', 'error');
finishAutoSolve();
}
}
}, 2000); // 增加等待时间到2秒
} else if (attempts >= maxAttempts) {
// 超时
clearInterval(waitForList);
updateStatus('返回列表页超时,请手动返回', 'error');
}
}, 500); // 每500ms检查一次
}, 2000); // 点击返回后等待2秒再开始轮询
} else {
updateStatus('未找到返回按钮,请手动返回列表页', 'error');
console.log('[DEBUG] 未找到返回按钮,当前按钮:', document.querySelectorAll('button').length);
}
}
// 完成自动刷题
function finishAutoSolve() {
autoSolving = false;
GM_setValue('rain_autosolving', false);
// 恢复UI
document.getElementById('rain-start-auto-btn').style.display = 'block';
document.getElementById('rain-stop-auto-btn').style.display = 'none';
document.getElementById('rain-auto-solve-list').style.opacity = '1';
document.getElementById('rain-select-all').disabled = false;
document.querySelectorAll('.rain-solve-checkbox').forEach(cb => {
cb.disabled = false;
cb.checked = false;
});
updateSelectedCount();
updateStatus('自动刷题完成!', 'success');
// 刷新题目列表
setTimeout(() => readQuestionList(), 1000);
}
// 停止自动刷题
function stopAutoSolve() {
autoSolving = false;
GM_setValue('rain_autosolving', false);
document.getElementById('rain-start-auto-btn').style.display = 'block';
document.getElementById('rain-stop-auto-btn').style.display = 'none';
document.getElementById('rain-auto-solve-list').style.opacity = '1';
document.getElementById('rain-select-all').disabled = false;
document.querySelectorAll('.rain-solve-checkbox').forEach(cb => cb.disabled = false);
updateStatus('已停止自动刷题', 'error');
}
function showAnswers(ans) {
const section = document.getElementById('rain-answers-section');
const container = document.getElementById('rain-answers');
if (section && container) {
section.style.display = 'block';
container.innerHTML = ans.map((a, i) => `
${i+1}
${a}
`).join('');
}
}
// ==================== 核心功能 ====================
function extractPageText() {
let result = '';
const leftPanel = document.querySelector('.question_left');
if (leftPanel) {
const titleEl = leftPanel.querySelector('.el-tabs__item.is-active') ||
document.querySelector('.font-size-20') ||
document.querySelector('h1, h2');
if (titleEl) {
result += '【文章标题】\n' + titleEl.innerText.trim() + '\n\n';
}
const directions = leftPanel.querySelector('.directions');
if (directions) {
let articleText = '';
let foundDirections = false;
const walker = document.createTreeWalker(leftPanel, NodeFilter.SHOW_TEXT, null, false);
let node;
while (node = walker.nextNode()) {
const text = node.textContent.trim();
if (foundDirections && text.length > 0) {
articleText += text + '\n';
}
if (node.parentElement?.closest('.directions')) {
foundDirections = true;
}
}
if (articleText.length > 100) {
result += '【文章内容】\n' + articleText.substring(0, 3000) + '\n\n';
}
}
}
if (result.length < 200) {
const articleTitle = document.querySelector('.font-size-20, .article-title');
const articleContent = document.querySelector('.line-height-28, .article-content');
if (articleTitle) {
result += '【文章标题】\n' + articleTitle.innerText.trim() + '\n\n';
}
if (articleContent) {
result += '【文章内容】\n' + articleContent.innerText.trim() + '\n\n';
}
}
const questionRight = document.querySelector('.question_right');
if (questionRight) {
result += '【题目】\n';
const questions = questionRight.querySelectorAll('.first_question');
questions.forEach((q, idx) => {
const qTextEl = q.querySelector('.question');
const qText = qTextEl?.innerText?.trim();
if (qText) {
const cleanQText = qText.replace(/^\d+[\.\.\、]\s*/, '');
result += (idx + 1) + '. ' + cleanQText + '\n';
const cards = q.querySelectorAll('.el-card__body');
cards.forEach(card => {
const optionText = card.innerText?.trim();
if (optionText && optionText.match(/^[ABCD][\.\.\、]/)) {
result += optionText + '\n';
}
});
result += '\n';
}
});
}
if (result.length < 200) {
const selectors = ['.el-main', '.main-content', '.content', 'main', '.question_box'];
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el && el.innerText.length > 500) {
result = cleanText(el.innerText);
break;
}
}
}
if (result.length < 200) {
result = cleanText(document.body.innerText);
}
console.log('[DEBUG] 提取内容长度:', result.length);
return result;
}
function cleanText(text) {
const panel = document.getElementById('rain-xichuan-container');
if (panel) {
text = text.replace(panel.innerText, '');
}
return text.trim();
}
function getQuestionCount() {
// 1. 检查阅读题新结构(.question_right)
const questionRight = document.querySelector('.question_right');
if (questionRight) {
let questions = questionRight.querySelectorAll('.first_question');
if (questions.length === 0) {
questions = questionRight.querySelectorAll('.question');
}
if (questions.length > 0) {
const hasArticle = document.querySelector('.line-height-28') !== null;
return {
count: questions.length,
type: hasArticle ? 'reading' : 'choice'
};
}
}
// 2. 检查输入框(填空题/单词变形)- 优先检测
const inputs = document.querySelectorAll('input.answer, input[name="answer[]"], input[type="text"]');
const visibleInputs = Array.from(inputs).filter(inp => {
const rect = inp.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
});
if (visibleInputs.length > 0) {
return { count: visibleInputs.length, type: 'fill' };
}
// 3. 检查传统选择题
try {
const text = document.body.innerText;
const matches = text.match(/\d+[\.\、\.]\s*[^\n]+[\n\s]*A[\.\、\.]/g);
if (matches && Array.isArray(matches) && matches.length > 0) {
const hasLongArticle = document.querySelectorAll('p').length > 5;
if (hasLongArticle && matches.length >= 3) {
const hasBlanks = text.match(/_{3,}|\(\s*\)/);
return { count: matches.length, type: hasBlanks ? 'cloze' : 'reading' };
}
return { count: matches.length, type: 'choice' };
}
} catch (e) {
console.log('[DEBUG] 正则匹配出错:', e);
}
return { count: 0, type: 'unknown' };
}
function buildPrompt(pageText, count, type) {
const typeNames = { reading: '阅读理解', cloze: '完形填空', choice: '选择题', fill: '填空题' };
const typeName = typeNames[type] || '题目';
let taskDesc = '';
if (type === 'reading' || type === 'cloze') {
taskDesc = '1. 仔细阅读文章\n2. 阅读每道题目和A/B/C/D四个选项\n3. 根据文章内容选择答案';
} else if (type === 'choice') {
taskDesc = '1. 仔细阅读每道题目和选项\n2. 选择最合适的答案';
} else {
taskDesc = '1. 识别每个填空的提示类型(首字母/中文/变形)\n2. 根据上下文填写答案';
}
const outputFormat = (type === 'reading' || type === 'cloze' || type === 'choice')
? '只输出选项字母,格式:1. A 或 A, B, C...'
: '只输出答案单词,格式:1. answer 或 answer1, answer2...';
return `你是一位专业的英语考试专家,精通${typeName}。
请完成以下${count}道${typeName}。
【题目内容】
${pageText.substring(0, 5000)}
【任务】
${taskDesc}
【输出格式】
${outputFormat}
不要任何解释,只输出答案:`;
}
async function callAI(prompt) {
if (!apiKey) throw new Error('请先配置API Key');
const config = MODEL_CONFIGS[currentModel];
let url = config.url;
let model = config.model;
if (currentModel === 'custom') {
url = customUrl;
model = customModel;
if (!url || !model) throw new Error('请配置自定义 API URL 和模型名');
}
// 特殊处理 Claude API
if (currentModel === 'claude') {
return callClaudeAPI(prompt, url, model);
}
// 特殊处理通义千问
if (currentModel === 'qwen') {
return callQwenAPI(prompt, url, model);
}
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: url,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
data: JSON.stringify({
model: model,
messages: [
{ role: 'system', content: '你是一位英语专家,只输出答案,不要解释。' },
{ role: 'user', content: prompt }
],
temperature: 0.3,
max_tokens: 2000
}),
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
if (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) {
resolve(data.choices[0].message.content);
} else if (data.error) {
reject(new Error('API错误: ' + (data.error.message || JSON.stringify(data.error))));
} else {
reject(new Error('API返回格式错误: ' + JSON.stringify(data).substring(0, 200)));
}
} catch (e) {
reject(new Error('解析失败: ' + e.message));
}
},
onerror: (err) => reject(new Error('请求失败: ' + (err.message || '未知错误')))
});
});
}
async function callClaudeAPI(prompt, url, model) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: url,
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01'
},
data: JSON.stringify({
model: model,
max_tokens: 2000,
messages: [
{ role: 'user', content: '你是一位英语专家,只输出答案,不要解释。\n\n' + prompt }
]
}),
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
if (data.content && data.content[0] && data.content[0].text) {
resolve(data.content[0].text);
} else if (data.error) {
reject(new Error('API错误: ' + (data.error.message || JSON.stringify(data.error))));
} else {
reject(new Error('API返回格式错误'));
}
} catch (e) {
reject(new Error('解析失败: ' + e.message));
}
},
onerror: (err) => reject(new Error('请求失败: ' + (err.message || '未知错误')))
});
});
}
async function callQwenAPI(prompt, url, model) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: url,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
data: JSON.stringify({
model: model,
input: {
messages: [
{ role: 'system', content: '你是一位英语专家,只输出答案,不要解释。' },
{ role: 'user', content: prompt }
]
},
parameters: {
temperature: 0.3,
max_tokens: 2000
}
}),
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
if (data.output && data.output.text) {
resolve(data.output.text);
} else if (data.error) {
reject(new Error('API错误: ' + (data.error.message || JSON.stringify(data.error))));
} else {
reject(new Error('API返回格式错误'));
}
} catch (e) {
reject(new Error('解析失败: ' + e.message));
}
},
onerror: (err) => reject(new Error('请求失败: ' + (err.message || '未知错误')))
});
});
}
function parseAnswers(response, count) {
const numbered = response.match(/^\s*\d+[\.\)]\s*(.+)$/gm);
if (numbered) {
return numbered.map(line => line.match(/^\s*\d+[\.\)]\s*(.+)$/)[1].trim()).slice(0, count);
}
const comma = response.split(/[,,]/).map(s => s.trim()).filter(s => s);
if (comma.length >= count) return comma.slice(0, count);
return response.split('\n').map(s => s.trim()).filter(s => s && !/^\d+$/.test(s)).slice(0, count);
}
async function startAnswering() {
if (isRunning) return;
isRunning = true;
answers = [];
try {
updateStatus('提取页面内容...');
const { count, type } = getQuestionCount();
const pageText = extractPageText();
console.log('[DEBUG] 提取内容长度:', pageText.length);
console.log('[DEBUG] 内容前1000字:', pageText.substring(0, 1000));
if (count === 0) {
updateStatus('未找到题目', 'error');
return;
}
updateStatus(`找到${count}道题,调用${MODEL_CONFIGS[currentModel].name}...`);
const prompt = buildPrompt(pageText, count, type);
const aiResponse = await callAI(prompt);
answers = parseAnswers(aiResponse, count);
updateStatus(`获取到${answers.length}个答案`, 'success');
showAnswers(answers);
// 根据题型设置按钮文字
const fillBtn = document.getElementById('rain-fill-btn');
const submitBtn = document.getElementById('rain-submit-btn');
if (type === 'fill') {
fillBtn.innerHTML = '✅ 填入答案';
submitBtn.innerHTML = '📝 提交答案';
} else {
fillBtn.innerHTML = '✅ 应用答案';
submitBtn.innerHTML = '📝 提交答案';
}
fillBtn.style.display = 'flex';
submitBtn.style.display = 'none'; // 默认隐藏提交按钮
} catch (err) {
updateStatus('错误: ' + err.message, 'error');
} finally {
isRunning = false;
}
}
// 模拟真实点击 - 通过 Vue 实例触发选择
function simulateRealClick(element) {
if (!element) return false;
try {
// 先移除同组其他选项的 right 类(单选)
const parent = element.closest('.first_question');
if (parent) {
parent.querySelectorAll('.el-card').forEach(card => {
card.classList.remove('right');
});
}
// 添加 right 类模拟选中(视觉反馈)
element.classList.add('right');
// 方法1: 尝试通过 Vue 实例触发点击
const vueInstance = element.__vue__;
if (vueInstance && vueInstance.$emit) {
vueInstance.$emit('click');
vueInstance.$emit('mousedown');
vueInstance.$emit('mouseup');
}
// 方法2: 触发内部元素的点击事件
const innerDiv = element.querySelector('.el-card__body > div');
if (innerDiv) {
// 触发原生点击事件
innerDiv.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
innerDiv.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
innerDiv.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
// 尝试触发 Vue 的点击处理
if (innerDiv.__vue__) {
innerDiv.__vue__.$emit('click');
}
}
// 方法3: 触发 el-card 本身的点击
element.click();
// 方法4: 触发 el-card__body 的点击
const cardBody = element.querySelector('.el-card__body');
if (cardBody) {
cardBody.click();
cardBody.dispatchEvent(new MouseEvent('click', { bubbles: true }));
}
return true;
} catch (e) {
console.log('[DEBUG] 点击失败:', e);
return false;
}
}
// 提交答案
function submitAnswers() {
let submitBtn = null;
// 1. 优先在题目区域查找(选择题在 .question_right,填空题可能在 .question_left 或其他地方)
const questionContainers = [
'.question_right',
'.question_left',
'.question_box',
'.el-main',
'.main-content'
];
for (const container of questionContainers) {
const parent = document.querySelector(container);
if (parent) {
// 在容器内查找按钮
const buttons = parent.querySelectorAll('button, .el-button, input[type="submit"]');
for (const btn of buttons) {
const text = (btn.innerText || btn.textContent || '').trim();
if (text.includes('提交') || text.includes('确认') || text.includes('完成') ||
btn.type === 'submit' || btn.classList.contains('el-button--primary')) {
submitBtn = btn;
console.log(`[DEBUG] 在 ${container} 中找到提交按钮:`, text);
break;
}
}
if (submitBtn) break;
}
}
// 2. 如果没找到,全局查找
if (!submitBtn) {
const allButtons = document.querySelectorAll('button, .el-button, input[type="submit"]');
for (const btn of allButtons) {
const text = (btn.innerText || btn.textContent || '').trim();
if (text.includes('提交') || text.includes('确认') || text.includes('完成')) {
submitBtn = btn;
console.log('[DEBUG] 全局找到提交按钮:', text);
break;
}
}
}
// 3. 还是没找到,尝试通用的 el-button--primary
if (!submitBtn) {
submitBtn = document.querySelector('.el-button--primary');
if (submitBtn) console.log('[DEBUG] 使用 el-button--primary');
}
if (submitBtn) {
console.log('[DEBUG] 点击提交按钮:', submitBtn);
submitBtn.click();
updateStatus('已提交答案', 'success');
// 标记当前题目完成
setTimeout(() => {
markCurrentCompleted();
}, 1000);
} else {
updateStatus('未找到提交按钮,请手动提交', 'error');
console.log('[DEBUG] 未找到提交按钮,所有按钮:',
Array.from(document.querySelectorAll('button')).map(b => b.innerText?.trim() || b.className));
}
}
function fillAnswers() {
// 使用传入的答案或全局保存的答案
const ansToFill = window.rainAnswers || answers;
const inputs = document.querySelectorAll('input.answer, input[name="answer[]"], input[type="text"]');
const visibleInputs = Array.from(inputs).filter(inp => {
const rect = inp.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
});
if (visibleInputs.length > 0) {
// 填空题 - 只填入答案,不自动提交
let filled = 0;
visibleInputs.forEach((input, i) => {
if (ansToFill[i]) {
input.value = ansToFill[i];
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
filled++;
}
});
updateStatus(`已填入${filled}个答案,请检查后再提交`, 'success');
// 显示提交按钮
const submitBtn = document.getElementById('rain-submit-btn');
if (submitBtn) submitBtn.style.display = 'flex';
} else {
// 选择题 - 尝试自动点击
let filled = 0;
const questions = document.querySelectorAll('.first_question');
// 禁用页面变化检测,防止选中后被重置
window._rainDisableReset = true;
ansToFill.forEach((ans, i) => {
if (!ans) return;
const letterMatch = ans.toString().toUpperCase().match(/[ABCD]/);
if (!letterMatch || !letterMatch[0]) return;
if (!questions[i]) return;
const cards = questions[i].querySelectorAll('.el-card');
const letter = letterMatch[0];
const idx = letter.charCodeAt(0) - 'A'.charCodeAt(0);
if (cards[idx]) {
// 尝试多种方式点击
simulateRealClick(cards[idx]);
filled++;
}
});
if (filled > 0) {
updateStatus(`已自动选择${filled}个答案,请点击提交按钮`, 'success');
// 显示提交按钮,让用户手动提交
const submitBtn = document.getElementById('rain-submit-btn');
if (submitBtn) submitBtn.style.display = 'flex';
} else {
updateStatus('未能选择任何答案', 'error');
}
// 5秒后恢复页面变化检测
setTimeout(() => {
window._rainDisableReset = false;
}, 5000);
}
}
// 检测页面变化并刷新
function setupPageChangeDetector() {
let lastUrl = location.href;
let lastQuestionCount = 0;
// 检测URL变化
const observer = new MutationObserver(() => {
const currentUrl = location.href;
if (currentUrl !== lastUrl) {
lastUrl = currentUrl;
console.log('[小rain的习传助手] 页面切换,重置状态...');
resetPanelState();
// 如果在题目详情页,尝试恢复批量模式
setTimeout(() => {
restoreBatchMode();
}, 500);
}
});
observer.observe(document, { subtree: true, childList: true });
// 定期检测题目数量变化(用于检测题目切换)
setInterval(() => {
const { count } = getQuestionCount();
if (count > 0 && count !== lastQuestionCount) {
console.log('[小rain的习传助手] 题目变化,重置状态...');
lastQuestionCount = count;
resetPanelState();
// 恢复批量模式UI
setTimeout(() => {
restoreBatchMode();
}, 500);
}
}, 2000);
}
// 恢复批量模式
function restoreBatchMode() {
if (!location.href.includes('specialtyDetail') && !location.href.includes('question')) {
return;
}
const savedList = GM_getValue('rain_question_list', '');
const savedIndex = GM_getValue('rain_current_index', 0);
const savedBatchMode = GM_getValue('rain_batch_mode', false);
// 只有当用户明确开启了批量模式时才恢复UI
// 如果没点过"读取题目列表",savedBatchMode为false,不显示批量UI
if (!savedBatchMode || !savedList) {
console.log('[DEBUG] 非批量模式,保持简洁界面');
// 确保批量区域隐藏
const batchSection = document.getElementById('rain-batch-section');
if (batchSection) batchSection.style.display = 'none';
// 只显示开始答题按钮
const readBtn = document.getElementById('rain-read-list-btn');
const startBtn = document.getElementById('rain-start-btn');
const semiAutoBtn = document.getElementById('rain-semi-auto-btn');
const autoSolveBtn = document.getElementById('rain-auto-solve-btn');
if (readBtn) readBtn.style.display = 'none';
if (startBtn) startBtn.style.display = 'flex';
if (semiAutoBtn) semiAutoBtn.style.display = 'none';
if (autoSolveBtn) autoSolveBtn.style.display = 'none';
return;
}
console.log('[DEBUG] 尝试恢复批量模式...');
// 重试几次,确保UI已渲染
let attempts = 0;
const maxAttempts = 10;
const tryRestore = () => {
attempts++;
try {
questionList = JSON.parse(savedList);
batchMode = true;
currentQuestionIndex = parseInt(savedIndex) || 0;
const batchSection = document.getElementById('rain-batch-section');
const readBtn = document.getElementById('rain-read-list-btn');
const startBtn = document.getElementById('rain-start-btn');
console.log('[DEBUG] 恢复尝试', attempts, '元素:', !!batchSection, !!readBtn, !!startBtn);
if (batchSection && readBtn && startBtn) {
batchSection.style.display = 'block';
readBtn.style.display = 'none';
startBtn.style.display = 'flex';
const autoSolveBtn = document.getElementById('rain-auto-solve-btn');
if (autoSolveBtn) autoSolveBtn.style.display = 'flex';
updateBatchUI();
console.log('[DEBUG] 恢复批量模式成功,当前题目:', currentQuestionIndex + 1);
return true;
}
} catch (e) {
console.log('[DEBUG] 恢复失败:', e);
}
if (attempts < maxAttempts) {
setTimeout(tryRestore, 300);
}
return false;
};
tryRestore();
}
// 重置面板状态
function resetPanelState(fullReset = true) {
// 如果禁用重置标志被设置,则跳过
if (window._rainDisableReset) {
console.log('[小rain的习传助手] 重置被禁用,跳过');
return;
}
answers = [];
isRunning = false;
// 重置UI
document.getElementById('rain-answers-section').style.display = 'none';
document.getElementById('rain-fill-btn').style.display = 'none';
document.getElementById('rain-submit-btn').style.display = 'none';
updateStatus('就绪 - 请开始答题');
// 清除之前的绿色标记
document.querySelectorAll('.rain-suggested').forEach(el => {
el.classList.remove('rain-suggested');
});
// 保存题目列表到存储
if (batchMode && questionList.length > 0) {
const saveData = questionList.map(q => ({
index: q.index,
title: q.title,
btnText: q.btnText,
isCompleted: q.isCompleted,
completed: q.completed
}));
GM_setValue('rain_question_list', JSON.stringify(saveData));
GM_setValue('rain_current_index', currentQuestionIndex);
}
}
// ==================== 批量答题功能 ====================
// 读取题目列表
function readQuestionList() {
// 查找表格行
const rows = document.querySelectorAll('.el-table__row');
if (rows.length === 0) {
updateStatus('未找到题目列表,请确保在题目列表页面', 'error');
return;
}
console.log('[DEBUG] 找到表格行数:', rows.length);
questionList = [];
rows.forEach((row, index) => {
const cells = row.querySelectorAll('td');
// 只处理有操作按钮的行(4个单元格的行)
if (cells.length !== 4) return;
// 4个单元格格式:标题、数量、分类、操作
const title = cells[0]?.innerText?.trim();
const btnCell = cells[3];
const clickTarget = btnCell?.querySelector('button');
if (title && clickTarget) {
const btnText = clickTarget.innerText?.trim();
const isCompleted = btnText?.includes('查看');
questionList.push({
index: index,
title: title,
btnText: btnText,
element: clickTarget,
isCompleted: isCompleted,
completed: false
});
}
});
if (questionList.length === 0) {
updateStatus('未能提取题目信息', 'error');
console.log('[DEBUG] 行数:', rows.length);
console.log('[DEBUG] 第一行单元格数:', rows[0]?.querySelectorAll('td').length);
console.log('[DEBUG] 第一行内容:', rows[0]?.innerHTML?.substring(0, 500));
return;
}
console.log('[DEBUG] 成功提取题目数:', questionList.length);
console.log('[DEBUG] 第一题:', questionList[0]);
// 显示批量答题区域
batchMode = true;
currentQuestionIndex = 0;
updateBatchUI();
// 标记为批量模式,保存到存储
GM_setValue('rain_batch_mode', true);
document.getElementById('rain-batch-section').style.display = 'block';
document.getElementById('rain-read-list-btn').style.display = 'none';
document.getElementById('rain-start-btn').style.display = 'flex';
document.getElementById('rain-auto-solve-btn').style.display = 'flex';
// 隐藏模式提示
const modeHint = document.getElementById('rain-mode-hint');
if (modeHint) modeHint.style.display = 'none';
// 过滤掉已完成的题目(只保留"做题"按钮的)
const todoList = questionList.filter(q => q.btnText?.includes('做题'));
const doneList = questionList.filter(q => q.btnText?.includes('查看'));
console.log('[DEBUG] 未做题:', todoList.length, '已做完:', doneList.length);
if (todoList.length === 0) {
updateStatus(`已读取 ${questionList.length} 道题目,全部已完成!`, 'success');
} else {
updateStatus(`已读取 ${questionList.length} 道题目,${todoList.length}道未做,点击开始答题`, 'success');
}
}
// 更新批量答题UI
function updateBatchUI() {
const progressEl = document.getElementById('rain-batch-progress');
const listEl = document.getElementById('rain-batch-list');
if (progressEl) {
const completed = questionList.filter(q => q.completed).length;
progressEl.textContent = `进度: ${completed}/${questionList.length} (当前: ${currentQuestionIndex + 1})`;
}
if (listEl) {
listEl.innerHTML = questionList.map((q, i) => `
${i + 1}. ${q.title.substring(0, 20)}${q.title.length > 20 ? '...' : ''}
${q.completed ? '✓' : (i === currentQuestionIndex ? '▶' : '○')}
`).join('');
// 点击题目跳转
listEl.querySelectorAll('.batch-item').forEach(item => {
item.onclick = () => {
const idx = parseInt(item.dataset.index);
enterQuestion(idx);
};
});
}
// 更新按钮状态
const prevBtn = document.getElementById('rain-prev-btn');
const nextBtn = document.getElementById('rain-next-btn');
if (prevBtn) prevBtn.disabled = currentQuestionIndex <= 0;
if (nextBtn) nextBtn.disabled = currentQuestionIndex >= questionList.length - 1;
}
// 从题目标题提取题型
function getQuestionType(title) {
if (title.includes('完形填空')) return '完形填空';
if (title.includes('单词变形')) return '单词变形';
if (title.includes('阅读理解')) return '阅读理解';
if (title.includes('单项选择')) return '单项选择';
return '';
}
// 进入指定题目
function enterQuestion(index) {
if (index < 0 || index >= questionList.length) return;
currentQuestionIndex = index;
let question = questionList[index];
// 提前获取题型,确保在else分支中也能使用
const qType = getQuestionType(question.title);
// 保存当前索引和题型
GM_setValue('rain_current_index', currentQuestionIndex);
if (qType) {
GM_setValue('rain_question_type', qType);
console.log('[DEBUG] 保存题型:', qType);
}
console.log('[DEBUG] 尝试进入题目:', question.title, '索引:', index, '有element:', !!question.element);
updateStatus(`正在进入第 ${index + 1} 题...`, 'success');
// 尝试获取或重新查找element
let btn = question.element;
// 如果没有element(可能在详情页返回后失效),尝试通过标题重新查找
if (!btn) {
console.log('[DEBUG] element失效,尝试重新查找...');
console.log('[DEBUG] 目标标题:', question.title);
const rows = document.querySelectorAll('.el-table__row');
console.log('[DEBUG] 表格行数:', rows.length);
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length !== 4) continue;
const title = cells[0]?.innerText?.trim();
const btnText = cells[3]?.querySelector('button')?.innerText?.trim();
// 优先匹配标题,如果标题匹配不上,就找第一个"做题"按钮
if (title === question.title) {
btn = cells[3]?.querySelector('button');
if (btn) {
question.element = btn;
console.log('[DEBUG] 通过标题重新找到element:', title);
break;
}
}
}
// 如果还没找到,找第一个未完成的题目(有"做题"按钮的)
if (!btn) {
console.log('[DEBUG] 标题匹配失败,尝试找第一个未完成的题目...');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length !== 4) continue;
const btnInCell = cells[3]?.querySelector('button');
const btnText = btnInCell?.innerText?.trim();
if (btnInCell && btnText && btnText.includes('做题')) {
btn = btnInCell;
console.log('[DEBUG] 找到第一个未完成的题目:', cells[0]?.innerText?.trim());
break;
}
}
}
}
// 如果找到element,点击它
if (btn) {
try {
// 方式1: 触发 Vue 点击事件(最有效)
if (btn.__vue__) {
btn.__vue__.$emit('click');
console.log('[DEBUG] 已触发Vue click事件');
}
// 方式2: 触发原生点击事件
setTimeout(() => {
const event = new MouseEvent('click', {
bubbles: true,
cancelable: true
});
btn.dispatchEvent(event);
}, 50);
// 自动刷题模式下,进入题目后自动开始答题
if (autoSolving) {
console.log('[DEBUG] 自动刷题模式,进入题目后3秒自动答题');
setTimeout(() => {
if (autoSolving) {
autoAnswerAndSubmit();
}
}, 3000);
}
} catch (e) {
console.log('[DEBUG] 进入题目失败:', e);
updateStatus('进入题目失败,请手动点击', 'error');
}
} else {
// 在详情页但没有element,提示用户手动返回
updateStatus('请在列表页点击题目', 'error');
}
updateBatchUI();
resetPanelState(false);
}
// 上一题
function goToPrevQuestion() {
let targetIndex = currentQuestionIndex - 1;
// 向前查找未完成的题目(跳过"查看"按钮的已做题目)
while (targetIndex >= 0) {
const q = questionList[targetIndex];
if (q?.completed || q?.btnText?.includes('查看')) {
targetIndex--;
} else {
break;
}
}
if (targetIndex >= 0) {
enterQuestion(targetIndex);
} else {
updateStatus('前面没有更多题目了', 'error');
}
}
// 下一题
function goToNextQuestion() {
let targetIndex = currentQuestionIndex + 1;
// 向后查找未完成的题目(跳过"查看"按钮的已做题目)
while (targetIndex < questionList.length) {
const q = questionList[targetIndex];
// 跳过:1) 标记为completed的 2) 按钮是"查看"的
if (q?.completed || q?.btnText?.includes('查看')) {
targetIndex++;
} else {
break;
}
}
if (targetIndex < questionList.length) {
enterQuestion(targetIndex);
} else {
updateStatus('已经是最后一题了', 'error');
}
}
// 标记当前题目完成并进入下一题
function markCurrentCompleted() {
if (batchMode && currentQuestionIndex < questionList.length) {
const currentQuestion = questionList[currentQuestionIndex];
currentQuestion.completed = true;
// 自动刷题模式下,记录已完成的题目标题
if (autoSolving && currentQuestion.title) {
completedTitles.add(currentQuestion.title);
console.log('[DEBUG] 记录已完成题目:', currentQuestion.title);
}
updateBatchUI();
// 自动刷题模式下,由 nextAutoSolve 处理进入下一题,这里不处理
if (autoSolving) {
console.log('[DEBUG] 自动刷题模式,跳过 autoNext 逻辑');
return;
}
if (autoNext && currentQuestionIndex < questionList.length - 1) {
updateStatus('本题已完成,2秒后自动进入下一题...', 'success');
setTimeout(() => {
goToNextQuestion();
}, 2000);
}
}
}
function init() {
console.log('[小rain的习传助手] v3.0 初始化...');
// 初始化时清除批量模式标记(只有点击读取列表后才开启)
GM_setValue('rain_batch_mode', false);
GM_setValue('rain_question_list', '');
createPanel();
setupPageChangeDetector();
// 检查是否在题目详情页,恢复批量模式
restoreBatchMode();
}
// 检查是否在题目列表页
function isQuestionListPage() {
const rows = document.querySelectorAll('.el-table__row');
return rows.length > 0;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();