// ==UserScript== // @name 党校智能答题助手 v1.2.0.1 // @namespace http://tampermonkey.net/ // @version 1.2.0.1 // @description 自动答题(反馈群:612441267) // @author lakay666 // @match *://*/jjfz/* // @match *://*/jfz/* // @match *://*/fzdx/* // @match *://*/lesson/exam* // @match *://*/exam_center/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @connect 150.158.119.55 // @connect localhost // @connect 127.0.0.1 // @connect * // @connect api.moonshot.cn // @run-at document-idle // ==/UserScript== (function() { 'use strict'; const SERVER_URL = 'http://150.158.119.55:3001'; const KIMI_API_URL = 'https://api.moonshot.cn/v1/chat/completions'; let KIMI_API_KEY = GM_getValue('kimi_api_key', ''); const wait = ms => new Promise(r => setTimeout(r, ms)); const log = (...args) => console.log('[党校助手]', ...args); // 全局控制变量 let isPaused = false; let answerLoopActive = false; let currentAnsweringIndex = 0; let totalQuestions = 0; let answeredQuestions = new Set(); let currentUser = null; // ==================== 通用请求 ==================== function gmFetch(url, options = {}) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: options.method || 'POST', url: url, headers: { 'Content-Type': 'application/json' }, data: options.body ? JSON.stringify(options.body) : undefined, timeout: options.timeout || 60000, onload: function(res) { try { let data = JSON.parse(res.responseText); resolve(data); } catch(e) { reject(new Error('解析失败')); } }, onerror: function(err) { reject(new Error('请求失败')); }, ontimeout: function() { reject(new Error('超时')); } }); }); } // ==================== 查找包含文本的链接 ==================== function findLinkByText(selector, text) { let el = document.querySelector(selector); if (el && el.textContent && el.textContent.includes(text)) { return el; } let allLinks = document.querySelectorAll('a'); for (let link of allLinks) { if (link.textContent && link.textContent.includes(text)) { return link; } } return null; } // ==================== 用户信息获取 ==================== function getUserInfo() { try { let platform_id = null, user_name = null; const uaId = document.cookie.split(';').find(c => c.includes('ua_id')); if (uaId) { let value = decodeURIComponent(uaId.split('=')[1]); let match = value.match(/eyJ[\w+/=]+/); if (match) { let data = JSON.parse(atob(match[0])); platform_id = String(data.user_id); user_name = data.user_name; } } if (!platform_id) { platform_id = '66666'; user_name = '大大大'; } // 统一学校格式:提取主域名 let hostname = location.hostname; let school = hostname.includes('uestc') ? 'uestc.edu.cn' : hostname; return { platform_id, user_name, school }; } catch(e) { return { platform_id: '32755', user_name: '常致君', school: 'uestc.edu.cn' }; } } // ==================== 题目提取 ==================== function extractQuestion() { let c = document.querySelector('.exam_cont_left'); if (!c) return { type: '单选题', title: '', options: [] }; let type = '单选题'; let typeEl = c.querySelector('.e_cont_title span'); if (typeEl) { let t = typeEl.textContent.trim(); if (t.includes('多选')) type = '多选题'; else if (t.includes('判断')) type = '判断题'; else if (t.includes('填空')) type = '填空题'; } let title = (c.querySelector('.exam_h2')?.innerText || '').replace(/^\d+[\.、]\s*/, '').trim(); let options = []; c.querySelectorAll('.answer_list li').forEach(li => { let text = li.querySelector('label')?.innerText?.trim() || li.innerText.trim(); text = text.replace(/^[A-D][、\.\s]*/i, '').trim(); if (text) options.push(text); }); return { type, title, options }; } // ==================== 答案选择 ==================== async function selectAnswer(type, answer, qnum) { if (answeredQuestions.has(qnum)) { log(`第${qnum}题已答过,跳过`); return; } let c = document.querySelector('.exam_cont_left'); if (!c) return; // ========== 填空题 ========== if (type === '填空题') { let input = c.querySelector('input[type="text"], textarea, .summary_question'); if (input) { input.value = ''; let nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; if (nativeSetter) { nativeSetter.call(input, answer); } else { input.value = answer; } input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('blur', { bubbles: true })); await wait(300); let saveBtn = null; let allBtns = document.querySelectorAll('a, button, .btn, input[type="button"]'); for (let btn of allBtns) { let text = btn.textContent || btn.value || ''; if (text.trim() === '保存' || text.includes('保存')) { saveBtn = btn; break; } } if (saveBtn) { saveBtn.click(); log(`✅ 填空题已保存: ${answer.substring(0, 30)}...`); await wait(1000); } else { log(`⚠️ 未找到保存按钮,答案已填写但未保存: ${answer.substring(0, 30)}...`); } } else { log(`❌ 未找到填空题输入框`); } answeredQuestions.add(qnum); return; } // ========== 判断题 ========== if (type === '判断题') { let labels = c.querySelectorAll('label'); let isCorrect = answer === '正确' || answer === '对' || answer === 'A' || answer === 'true'; for (let label of labels) { let text = label.textContent.trim(); if (isCorrect && (text.includes('正确') || text.includes('对'))) { let radio = label.querySelector('input[type="radio"]'); if (!radio || !radio.checked) label.click(); answeredQuestions.add(qnum); log(`✅ 判断题已选: ${isCorrect ? '正确' : '错误'}`); break; } else if (!isCorrect && (text.includes('错误') || text.includes('错'))) { let radio = label.querySelector('input[type="radio"]'); if (!radio || !radio.checked) label.click(); answeredQuestions.add(qnum); log(`✅ 判断题已选: ${isCorrect ? '正确' : '错误'}`); break; } } return; } // ========== 单选题 ========== if (type !== '多选题') { let items = c.querySelectorAll('.answer_mul, .answer_list li'); let idx = -1; if (answer.match(/^[A-D]$/i)) idx = answer.toUpperCase().charCodeAt(0) - 65; else if (answer.match(/^\d+$/)) idx = parseInt(answer) - 1; if (idx >= 0 && idx < items.length) { let radio = items[idx].querySelector('input[type="radio"]'); if (radio && !radio.checked) { radio.click(); answeredQuestions.add(qnum); log(`✅ 单选题已选: ${answer}`); } } return; } // ========== 多选题 ========== let items = c.querySelectorAll('.answer_mul'); let answers = answer.toUpperCase().split(''); for (let i = 0; i < items.length; i++) { if (answers.includes(String.fromCharCode(65 + i))) { let cb = items[i].querySelector('input[type="checkbox"]'); if (cb && !cb.checked) { cb.click(); await wait(200); } } } answeredQuestions.add(qnum); log(`✅ 多选题已选: ${answer}`); } // ==================== 采集答案(修复填空题) ==================== async function collectQuestions() { log('========== 开始采集答案 =========='); let questions = []; let blocks = document.querySelectorAll('.error_sub'); log(`找到 ${blocks.length} 个题目块`); for (let block of blocks) { let h3 = block.querySelector('h3'); if (!h3) continue; let fullTitle = h3.innerText.trim(); let type = '未知'; if (fullTitle.includes('单选题')) type = '单选题'; else if (fullTitle.includes('多选题')) type = '多选题'; else if (fullTitle.includes('判断题')) type = '判断题'; else if (fullTitle.includes('填空题')) type = '填空题'; let title = fullTitle.replace(/^\d+[、,.]\s*/, '').replace(/【.+?】/, '').trim(); let options = []; let optContainer = block.querySelector('.exam_result2, .exam_result_box2'); if (optContainer) { optContainer.querySelectorAll('li').forEach((li, idx) => { let text = li.innerText.trim().replace(/^[A-D][、,.\s]*/i, ''); if (text) options.push(String.fromCharCode(65 + options.length) + '. ' + text); }); } let answer = ''; if (type === '填空题') { // 填空题答案在 .sub_cont 中 let subCont = block.querySelector('.sub_cont'); if (subCont) { answer = subCont.innerText.trim(); } if (!answer) { let subColor = block.querySelector('.sub_color'); if (subColor) { let ansText = subColor.innerText; let m = ansText.match(/正确答案[::]\s*(.+?)(?:\n|$)/); if (m) answer = m[1].trim(); } } if (!answer) { let match = block.innerText.match(/答[::]\s*(.+?)(?:\n|$)/); if (match) answer = match[1].trim(); } } else { let ansEl = block.querySelector('.sub_color'); if (ansEl) { let ansText = ansEl.innerText; let m = ansText.match(/正确答案[::]\s*([A-D]+|正确|错误)/i); if (m) { answer = m[1]; } else if (ansText.includes('正确')) { answer = '正确'; } else if (ansText.includes('错误')) { answer = '错误'; } else { let letterMatch = ansText.match(/([A-D]{2,})/); if (letterMatch) answer = letterMatch[1]; } } if (!answer && optContainer) { let selected = ''; let optLis = optContainer.querySelectorAll('li'); optLis.forEach((li, i) => { if (li.classList.contains('result_cut') || li.querySelector('.checked')) { selected += String.fromCharCode(65 + i); } }); if (selected) answer = selected; } } if (title && answer) { questions.push({ type, title, options, answer }); log(`✅ 采集: ${type} | ${title.substring(0, 40)}... → ${answer.substring(0, 30)}`); } else { log(`⚠️ 跳过: ${fullTitle.substring(0, 50)} (答案: ${answer || '无'})`); } } log(`📊 共采集 ${questions.length} 题`); if (questions.length > 0) { try { // 统一学校格式 let hostname = location.hostname; let school = hostname.includes('uestc') ? 'uestc.edu.cn' : hostname; let res = await gmFetch(SERVER_URL + '/save-batch', { body: { questions, school: school }, timeout: 60000 }); log(`✅ 保存成功: 新增 ${res.saved || 0} 题, 跳过 ${res.skipped || 0} 题`); } catch(e) { log('❌ 保存失败:', e); } } log('========== 采集完成 =========='); } // ==================== 自动查看详情 ==================== function autoClickDetail() { log('开始查找查看考试详情按钮...'); let detailBtn = document.querySelector('a.submit_btn2'); if (detailBtn) { log('通过 class 找到按钮'); detailBtn.click(); return true; } let allLinks = document.querySelectorAll('a'); for (let link of allLinks) { let text = link.textContent || ''; if (text.includes('查看考试详情') || text.includes('查看详情') || text.includes('详情')) { log(`通过文本找到按钮: "${text}"`); link.click(); return true; } } for (let link of allLinks) { let href = link.getAttribute('href') || ''; if (href.includes('/show') || href.includes('/end_show') || href.includes('show?rid')) { log(`通过 href 找到按钮: ${href}`); link.click(); return true; } } log('未找到查看详情按钮'); return false; } // ==================== 交卷并跳转采集 ==================== async function submitAndCollect() { log('开始交卷...'); let submitBtn = document.getElementById('submit_exam') || document.querySelector('.exam_a_sub'); if (submitBtn) submitBtn.click(); await wait(800); let confirmBtn = document.querySelector('.public_submit'); if (confirmBtn && confirmBtn.offsetParent !== null) { confirmBtn.click(); log('已确认交卷'); } else { let btns = document.querySelectorAll('a, button'); for (let btn of btns) { if (btn.textContent && btn.textContent.includes('我要提交') && btn.offsetParent !== null) { btn.click(); log('已确认交卷(备用)'); break; } } } log('等待跳转到结果页...'); let checkInterval = setInterval(() => { if (location.href.includes('/result')) { clearInterval(checkInterval); log('进入结果页,准备点击查看详情'); setTimeout(() => { let clicked = autoClickDetail(); if (clicked) { setTimeout(() => { if (location.href.includes('/show') || location.href.includes('/end_show')) { log('进入答案页,开始采集'); setTimeout(() => collectQuestions(), 2000); } }, 2000); } }, 1500); } }, 500); setTimeout(() => clearInterval(checkInterval), 10000); } // ==================== 本地AI答案 ==================== async function localKimiAnswer(title, options, type) { if (!KIMI_API_KEY) return null; let prompt = `请回答以下${type}。\n题目:${title}\n`; if (type !== '填空题' && type !== '判断题') { prompt += `选项:\n${options.map((o,i) => String.fromCharCode(65+i) + '. ' + o).join('\n')}\n`; } prompt += type === '填空题' ? '直接输出答案:' : '只输出选项字母:'; try { let res = await gmFetch(KIMI_API_URL, { method: 'POST', headers: { 'Authorization': 'Bearer ' + KIMI_API_KEY }, body: { model: 'moonshot-v1-8k', messages: [{ role: 'user', content: prompt }], temperature: 0.1, max_tokens: 20 } }); let raw = res.choices?.[0]?.message?.content?.trim() || ''; if (type === '判断题') return raw.includes('正确') ? '正确' : '错误'; if (type === '多选题') return raw.replace(/[^A-Z]/gi, '').toUpperCase(); return raw.replace(/[^A-D]/gi, '').toUpperCase()[0] || ''; } catch(e) { log('本地AI失败:', e.message); return null; } } // ==================== 主答题函数 ==================== async function runAutoAnswer() { if (answerLoopActive) { log('已有答题循环在运行,先停止旧的'); isPaused = true; await wait(1000); answerLoopActive = false; } answerLoopActive = true; isPaused = false; let status = document.getElementById('hzau-status'); currentUser = getUserInfo(); log('用户:', currentUser); let lis = []; document.querySelectorAll('ul.exam_ul li').forEach(li => { if (/^\d+$/.test(li.textContent.trim())) lis.push(li); }); totalQuestions = lis.length; if (totalQuestions === 0) { if(status) status.textContent = '❌ 未找到题目'; log('未找到题目列表'); answerLoopActive = false; return; } log(`共 ${totalQuestions} 题,开始答题...`); for (let i = 0; i < lis.length; i++) { while (isPaused) { await wait(500); } currentAnsweringIndex = i + 1; let li = lis[i]; let num = parseInt(li.textContent); if (answeredQuestions.has(num)) { log(`第${num}题已答过,跳过`); continue; } if(status) status.textContent = `${num}/${totalQuestions} 题...`; li.click(); await wait(1500); let q = extractQuestion(); log(`第${num}题 [${q.type}] ${q.title.substring(0, 40)}`); let answer = null; let retryCount = 0; while (!answer && retryCount < 2) { try { let res = await gmFetch(SERVER_URL + '/api/exam/answer', { body: { platform_id: currentUser.platform_id, user_name: currentUser.user_name, type: q.type, title: q.title, options: q.options, school: currentUser.school }, timeout: 30000 }); if (res.error) { log('服务器错误:', res.error); } else { answer = res.answer; if(status) status.textContent = `${num}/${totalQuestions} - 剩余${res.remaining || '?'}次`; break; } } catch(e) { log(`请求失败 (尝试${retryCount+1}/2):`, e.message); } if (!answer && KIMI_API_KEY) { answer = await localKimiAnswer(q.title, q.options, q.type); if (answer) log('使用本地AI答案'); } retryCount++; if (!answer && retryCount < 2) await wait(2000); } if (!answer) { log(`第${num}题无法获取答案,跳过`); continue; } log(`答案: ${answer}`); await selectAnswer(q.type, answer, num); await wait(800); } answerLoopActive = false; if(status) status.textContent = `✅ 完成 ${totalQuestions} 题`; log(`答题完成,共答 ${answeredQuestions.size} 题`); if (!isPaused) { await wait(1000); await submitAndCollect(); } } // ==================== 暂停/恢复/重置 ==================== function pauseAnswer() { if (!isPaused && answerLoopActive) { isPaused = true; log(`⏸️ 答题已暂停,当前进度: ${currentAnsweringIndex}/${totalQuestions}`); let status = document.getElementById('hzau-status'); if(status) status.textContent = `⏸️ 已暂停 (${currentAnsweringIndex}/${totalQuestions})`; } } function resumeAnswer() { if (isPaused) { isPaused = false; log('▶️ 答题已恢复,继续当前进度'); let status = document.getElementById('hzau-status'); if(status) status.textContent = `▶️ 继续答题... 第${currentAnsweringIndex}/${totalQuestions}题`; } } function resetProgress() { isPaused = false; answerLoopActive = false; answeredQuestions.clear(); currentAnsweringIndex = 0; totalQuestions = 0; log('🔄 答题进度已重置'); let status = document.getElementById('hzau-status'); if(status) status.textContent = '就绪'; } // ==================== 控制面板 ==================== function initPanel() { let user = getUserInfo(); (async () => { try { let res = await gmFetch(SERVER_URL + '/api/user/info?platform_id=' + user.platform_id + '&user_name=' + encodeURIComponent(user.user_name) + '&school=' + user.school, { method: 'GET' }); if (!res.error && document.getElementById('remaining-num')) { document.getElementById('remaining-num').innerText = res.answer_times || '?'; } } catch(e) {} })(); let panel = document.createElement('div'); panel.id = 'hzau-panel'; panel.innerHTML = `
🤖 党校助手 v1.2.0.1
🏫 ${user.school}
👤 ${user.user_name} | 剩余: ...
就绪
`; document.body.appendChild(panel); document.getElementById('hzau-auto').onclick = () => { resetProgress(); runAutoAnswer(); }; document.getElementById('hzau-pause').onclick = () => pauseAnswer(); document.getElementById('hzau-resume').onclick = () => resumeAnswer(); document.getElementById('hzau-submit').onclick = () => submitAndCollect(); document.getElementById('hzau-reset').onclick = () => resetProgress(); } // ==================== 页面入口 ==================== const currentUrl = location.href; if (currentUrl.includes('/show') || currentUrl.includes('/end_show')) { log('检测到答案页,自动采集'); setTimeout(() => collectQuestions(), 2000); } else if (currentUrl.includes('/result')) { log('检测到结果页,自动查看详情'); setTimeout(() => { autoClickDetail(); setTimeout(() => { if (location.href.includes('/show') || location.href.includes('/end_show')) { log('进入答案页,开始采集'); setTimeout(() => collectQuestions(), 2000); } }, 3000); }, 2000); } else if (currentUrl.includes('/exam') || currentUrl.includes('/lesson/exam')) { initPanel(); } GM_registerMenuCommand('🔑 设置本地Kimi Key', () => { let key = prompt('请输入 Kimi API Key(留空使用服务器):', KIMI_API_KEY); if (key === '') { GM_setValue('kimi_api_key', ''); KIMI_API_KEY = ''; alert('✅ 已切换到服务器模式'); } else if (key && key.startsWith('sk-')) { GM_setValue('kimi_api_key', key); KIMI_API_KEY = key; alert('✅ 本地Key已保存'); } }); })();