// ==UserScript== // @name 🤖 党旗飘飘 党校智能答题助手 v1.1.5 // @namespace http://tampermonkey.net/ // @version 1.1.5 // @description 自动答题(反馈群:612441267) // @author lakay666 // @match https://wsdx.hzau.edu.cn/jjfz/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @connect 150.158.119.55 // @connect wsdx.hzau.edu.cn // @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); GM_registerMenuCommand('🔑 设置本地Kimi Key(留空=用服务器)', () => { const key = prompt('请输入 Kimi API Key(sk-开头,留空使用服务器默认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已保存'); } else if (key) { alert('❌ Key 格式错误'); } }); function gmFetch(url, options = {}) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: options.method || 'GET', url, headers: { 'Content-Type': 'application/json', ...options.headers }, data: options.body ? JSON.stringify(options.body) : undefined, timeout: 15000, onload: res => { try { resolve(JSON.parse(res.responseText)); } catch { resolve(res.responseText); } }, onerror: reject, ontimeout: () => reject(new Error('超时')) }); }); } function getUserInfo() { try { const uaId = document.cookie.split(';').find(c => c.includes('ua_id')); if (!uaId) return null; const raw = decodeURIComponent(uaId.split('=')[1]); const match = raw.match(/eyJ[\w+/=]+/); if (!match) return null; const json = JSON.parse(atob(match[0])); return { platform_id: String(json.user_id), user_name: json.user_name }; } catch (e) { return null; } } async function localKimiAnswer(title, options, type) { const prompt = '请回答以下' + type + '。\n题目:' + title + '\n' + (type === '填空题' ? '请直接输出答案文字,不要解释。' : type === '判断题' ? '请只输出"正确"或"错误"两个汉字。' : '选项:\n' + options.map((o,i) => String.fromCharCode(65+i) + '. ' + o).join('\n') + '\n要求:' + (type === '多选题' ? '输出所有正确选项字母如AB' : '输出一个字母如C')); const 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: type === '填空题' ? 50 : 10 } }); if (res.error) throw new Error(res.error.message); const raw = res.choices?.[0]?.message?.content?.trim() || ''; if (type === '填空题') return raw; if (type === '判断题') { const r = raw.replace(/[。!,\s]/g, ''); if (r.includes('正确') || r.includes('对')) return '正确'; if (r.includes('错误') || r.includes('错')) return '错误'; return '正确'; } if (type === '多选题') return raw.replace(/[^A-D]/g, '').toUpperCase(); return raw.replace(/[^A-D]/g, '').toUpperCase()[0] || ''; } // ==================== 页面判断 ==================== const href = location.href; const isExamPage = href.includes('/lesson/exam') || href.includes('/exam_center/end_exam'); const isResultPage = href.includes('/lesson/exam/result') || href.includes('/exam_center/result'); const isRecordPage = href.includes('/exam_center/record'); const isShowPage = href.includes('/exam_center/show?rid=') || href.includes('/exam_center/end_show?rid='); if (isExamPage) initExamPage(); else if (isResultPage) initResultPage(); else if (isRecordPage || isShowPage) initRecordPage(); // ==================== 答题页 ==================== async function initExamPage() { const user = getUserInfo(); const panel = document.createElement('div'); panel.id = 'hzau-panel'; panel.style.cssText = 'position:fixed;top:20px;right:20px;z-index:999999;background:#fff;border:2px solid #e74c3c;border-radius:8px;padding:15px;width:280px;box-shadow:0 4px 15px rgba(0,0,0,0.3);font-family:"Microsoft YaHei",sans-serif;font-size:13px;'; let infoHtml = ''; if (user) { try { const info = await gmFetch(SERVER_URL + '/api/user/info?platform_id=' + user.platform_id + '&user_name=' + encodeURIComponent(user.user_name)); infoHtml = '
👤 ' + user.user_name + ' | 剩余: ' + (info.answer_times ?? '?') + '
'; } catch (e) { infoHtml = '
⚠️ 服务器连接失败
'; } } else { infoHtml = '
⚠️ 未检测到登录信息
'; } panel.innerHTML = '
🤖 党校助手 v1.1.5
' + infoHtml + '
'; document.body.appendChild(panel); document.getElementById('hzau-auto').onclick = async () => { const btn = document.getElementById('hzau-auto'); btn.disabled = true; btn.textContent = '⏳ 运行中...'; try { await runAutoAnswer(true); } catch (e) { alert(e.message); } btn.disabled = false; btn.textContent = '🚀 全自动答题'; }; document.getElementById('hzau-submit').onclick = clickSubmit; } function killInterference() { const maxId = setTimeout(() => {}, 0); for (let i = 0; i < maxId; i++) { try { clearTimeout(i); clearInterval(i); } catch(e) {} } } async function runAutoAnswer(autoSubmit) { const status = document.getElementById('hzau-status'); const user = getUserInfo(); if (!user) { alert('未检测到登录信息,请先登录党校平台'); return; } const lessonId = new URLSearchParams(location.search).get('lesson_id') || ''; const allUls = document.querySelectorAll('ul.exam_ul'); const allLis = []; allUls.forEach(ul => { ul.querySelectorAll('li').forEach(li => { const t = li.textContent.trim(); if (/^\d+$/.test(t)) allLis.push({ num: parseInt(t), el: li, qid: li.id }); }); }); const total = allLis.length; log('共 ' + total + ' 题 | ' + (KIMI_API_KEY ? '本地AI优先' : '服务器模式')); for (let i = 0; i < allLis.length; i++) { const { num, el } = allLis[i]; status.textContent = '第 ' + num + '/' + total + ' 题...'; el.click(); await waitForQuestionLoad(); const q = extractQuestion(); log('第' + num + '题 [' + q.type + '] ' + q.title.slice(0, 30)); let answer = null; let source = ''; // 1. 本地AI(带超时保护) if (KIMI_API_KEY) { try { answer = await Promise.race([ localKimiAnswer(q.title, q.options, q.type), new Promise((_, reject) => setTimeout(() => reject(new Error('本地AI超时')), 8000)) ]); source = '🤖本地AI'; } catch (e) { log(' 本地AI: ' + e.message); answer = null; } } // 2. 服务器(带超时保护) if (!answer) { try { const res = await Promise.race([ gmFetch(SERVER_URL + '/api/exam/answer', { method: 'POST', body: { platform_id: user.platform_id, user_name: user.user_name, lesson_id: lessonId, type: q.type, title: q.title, options: q.options } }), new Promise((_, reject) => setTimeout(() => reject(new Error('服务器超时')), 10000)) ]); if (res.error) { alert(res.error); return; } answer = res.answer; source = '🖥️服务器'; status.textContent = '第 ' + num + '/' + total + ' - 剩余' + (res.remaining ?? '?') + '次'; } catch (e) { log(' 服务器: ' + e.message); continue; } } // 判断题转换 if (q.type === '判断题' && answer && /^[A-D]$/.test(answer)) { const labels = document.querySelectorAll('.exam_cont_left .answer_list li label'); if (labels.length === 2) { const idx = answer.charCodeAt(0) - 65; const text = labels[idx]?.textContent.trim() || ''; if (text.includes('正确') || text.includes('对')) answer = '正确'; else if (text.includes('错误') || text.includes('错')) answer = '错误'; } } log(' ' + source + ': ' + answer); killInterference(); await selectAnswer(q.type, answer); await wait(300); } status.textContent = '✅ 完成 ' + total + ' 题'; if (autoSubmit) { status.textContent = '正在交卷...'; await wait(1000); clickSubmit(); } } function extractQuestion() { const c = document.querySelector('.exam_cont_left'); const typeEl = c.querySelector('.e_cont_title span'); let type = typeEl?.textContent?.trim() || '未知'; if (!type || type === '未知') { if (c.querySelector('.summary_question')) type = '填空题'; } let title = c.querySelector('.exam_h2')?.innerText?.trim() || ''; title = title.replace(/^\d+\.\s*/, '').replace(/\u00A0/g, ' '); let optEls = c.querySelectorAll('.answer_list li, .answer_list_box li'); const options = []; optEls.forEach(li => { const label = li.querySelector('label'); let text = label ? label.innerText.trim() : li.innerText.trim(); text = text.replace(/^\s+/, '').trim(); if (text) options.push(text); }); return { type, title, options }; } async function selectAnswer(type, answer) { const c = document.querySelector('.exam_cont_left'); if (!c) return; // 填空题 if (type === '填空题') { const input = c.querySelector('.summary_question, input[type="text"], textarea'); if (input) { input.focus(); input.value = ''; input.dispatchEvent(new Event('input', { bubbles: true })); await wait(100); input.value = answer; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('blur', { bubbles: true })); } const saveBtn = [...document.querySelectorAll('a')].find(a => a.textContent.trim() === '保存'); if (saveBtn) { saveBtn.click(); saveBtn.dispatchEvent(new MouseEvent('click', { bubbles: true })); } await wait(600); const nextBtn = document.getElementById('next_question'); if (nextBtn && !nextBtn.classList.contains('dis_click')) nextBtn.click(); return; } // 判断题 if (type === '判断题') { c.querySelectorAll('label').forEach(label => { const text = label.textContent.trim(); if ((answer === '正确' && (text.includes('正确') || text.includes('对'))) || (answer === '错误' && (text.includes('错误') || text.includes('错')))) { label.click(); } }); return; } // 单选题 if (type !== '多选题') { let items = c.querySelectorAll('.answer_mul'); if (items.length === 0) items = c.querySelectorAll('.answer_list li'); const idx = answer.charCodeAt(0) - 65; if (idx >= 0 && idx < items.length) { const label = items[idx].querySelector('label'); if (label) label.click(); } return; } // 多选题 const items = c.querySelectorAll('.answer_mul'); if (items.length === 0) return; const answers = answer.split(''); const timestamp = Date.now(); items.forEach((item, i) => { const input = item.querySelector('input[type="checkbox"]'); if (input) { input.checked = false; input.name = 'multi_' + timestamp + '_' + i; } }); for (let idx = items.length - 1; idx >= 0; idx--) { const letter = String.fromCharCode(65 + idx); if (answers.includes(letter)) { const item = items[idx]; const input = item.querySelector('input'); const label = item.querySelector('label'); input.name = 'multi_' + timestamp + '_fix_' + idx; input.checked = true; input.dispatchEvent(new Event('change', { bubbles: true })); if (label) { label.classList.add('layui-form-checked'); label.click(); } await wait(600); } } } function waitForQuestionLoad() { return new Promise(resolve => { let resolved = false; const c = document.querySelector('.exam_cont_left'); if (c) { const obs = new MutationObserver(() => { if (!document.querySelector('.layui-layer-loading') && (c.querySelector('.exam_h2') || c.querySelector('.summary_question') || c.querySelector('.answer_list li')) && !resolved) { resolved = true; obs.disconnect(); resolve(); } }); obs.observe(c, { childList: true, subtree: true }); } setTimeout(() => { if (!resolved) resolve(); }, 2000); }); } function clickSubmit() { const btn = document.getElementById('submit_exam') || document.querySelector('.exam_a_sub'); if (btn) { btn.click(); setTimeout(confirmSubmitDialog, 500); return true; } for (const el of document.querySelectorAll('a, button')) { if (/^交卷$|^提交$/.test(el.textContent.trim())) { el.click(); setTimeout(confirmSubmitDialog, 500); return true; } } return false; } function confirmSubmitDialog() { const btn = document.querySelector('.public_submit'); if (btn) { btn.click(); return true; } for (const el of document.querySelectorAll('a, button')) { if (el.textContent.includes('我要提交') || el.textContent.includes('确认')) { el.click(); return true; } } return false; } // ==================== 自动收录 ==================== function initResultPage() { log('📚 自动收录...'); setTimeout(async () => { const btn = Array.from(document.querySelectorAll('a, button')).find(el => el.textContent.includes('详情') || (el.href || '').includes('show')); if (btn) { btn.click(); await wait(2500); } await saveQuestions(); }, 2000); } function initRecordPage() { log('📚 自动收录...'); setTimeout(async () => { await saveQuestions(); }, 2000); } async function saveQuestions() { const questions = []; document.querySelectorAll('.error_sub').forEach((el, idx) => { const h3 = el.querySelector('h3'); let title = h3 ? h3.innerText.trim() : ''; const typeMatch = title.match(/【(.+?)】/); const type = typeMatch ? typeMatch[1] : '未知'; title = title.replace(/^\d+[、,.]\s*/, '').replace(/【.+?】/, '').trim(); const optEls = (el.querySelector('.exam_result2, .exam_result_box2') || el).querySelectorAll('li'); const options = []; optEls.forEach(li => { const text = li.innerText.trim().replace(/^[A-D][、,]?\s*/, '').trim(); if (text) options.push(String.fromCharCode(65 + options.length) + '. ' + text); }); let answer = ''; if (type === '填空题') { const contEl = el.querySelector('.sub_cont'); if (contEl) answer = contEl.textContent.trim(); } else { const ansEl = el.querySelector('.sub_color'); if (ansEl) { const m = ansEl.textContent.match(/正确答案[::]\s*([A-D]+|正确|错误)/); answer = m ? m[1] : ''; } if (!answer) optEls.forEach((li, i) => { if (li.classList.contains('result_cut') || li.querySelector('.checked')) answer += String.fromCharCode(65 + i); }); } if (title && answer) questions.push({ qid: 'a_' + idx + '_' + Date.now(), type, title, options, answer }); }); if (questions.length > 0) { try { const res = await gmFetch(SERVER_URL + '/save-batch', { method: 'POST', body: { questions } }); log('收录: 新增' + (res.saved || 0) + ' 跳过' + (res.skipped || 0)); } catch (e) { log('收录失败:', e); } } } })();