// ==UserScript== // @name 悬壶自动刷 // @namespace http://tampermonkey.net/ // @version 1.2.4 // @description 人卫智网增值服务+考试 练习题批量获取、LLM自动答题、一键保存全书习题 // @author WorkBuddy // @match https://zengzhi.ipmph.com/answer/* // @match https://zengzhi.ipmph.com/* // @match https://tk.ipmph.com/exam/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_download // @grant GM_openInTab // @grant unsafeWindow // @connect * // @run-at document-idle // @license MIT // @homepage https://github.com/GeorgeChou17/xuanshu-auto // @supportURL https://github.com/GeorgeChou17/xuanshu-auto/issues // ==/UserScript== (function() { 'use strict'; // ===================== 免责声明(首次安装弹出) ===================== var DISCLAIMER_VERSION = '1.0'; var DISCLAIMER_KEY = 'xuanhu_disclaimer_agreed'; if (typeof GM_getValue !== 'undefined' && !GM_getValue(DISCLAIMER_KEY, null)) { var disclaimerText = '【悬壶自动刷 — 免责声明】\n\n' + '1. 技术学习目的:本脚本完全基于JavaScript编程语言的学习与技术交流之目的开发,开发者从未以任何形式鼓励、诱导或帮助任何使用者侵犯他人知识产权。\n\n' + '2. 使用者行为责任:使用者须明确知悉,通过本脚本获取的任何内容(包括但不限于试题、文本)的知识产权均归原权利方(人民卫生出版社)所有。使用者承诺仅将本脚本用于合法的、非商业的本地技术测试环境,并应在测试完成后立即删除任何获取的临时数据。\n\n' + '3. 风险自担与责任排除:使用者已知悉本脚本可能违反目标网站的《用户服务协议》,并自愿承担因使用本脚本而可能引发的一切法律风险、账号封禁、数据丢失等后果。开发者不对因使用本脚本而产生的任何直接或间接损失承担法律责任,一切由本脚本引发的侵权及合规风险,均由使用者本人独立承担。\n\n' + '点击"确认"表示您已阅读、理解并同意上述声明。'; var agreed = confirm(disclaimerText); if (agreed) { GM_setValue(DISCLAIMER_KEY, DISCLAIMER_VERSION); } else { // 用户不同意,停止脚本运行 return; } } // ===================== 样式 ===================== GM_addStyle( '#ipmph-panel{position:fixed;top:10px;right:10px;width:400px;background:#fff;border:2px solid #1565C0;border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,.18);z-index:999999;font-family:system-ui,sans-serif;font-size:14px;color:#333}' + '#ipmph-panel.collapsed .body{display:none}' + '#ipmph-panel .hdr{background:linear-gradient(135deg,#1565C0,#0D47A1);color:#fff;padding:12px 16px;border-radius:10px 10px 0 0;display:flex;justify-content:space-between;align-items:center;cursor:move;user-select:none}' + '#ipmph-panel .hdr h3{margin:0;font-size:15px;font-weight:600}' + '#ipmph-panel .hdr-btns{display:flex;gap:6px}' + '#ipmph-panel .hdr-btns button{background:rgba(255,255,255,.2);border:none;color:#fff;font-size:16px;cursor:pointer;padding:2px 8px;border-radius:4px}' + '#ipmph-panel .hdr-btns button:hover{background:rgba(255,255,255,.35)}' + '#ipmph-panel .body{padding:14px 16px;max-height:75vh;overflow-y:auto}' + '#ipmph-panel .sec{margin-bottom:14px;padding-bottom:14px;border-bottom:1px solid #e0e0e0}' + '#ipmph-panel .sec:last-child{border-bottom:none;margin-bottom:0;padding-bottom:0}' + '#ipmph-panel .sec-t{font-size:13px;font-weight:600;color:#1565C0;margin-bottom:8px;cursor:pointer;display:flex;justify-content:space-between;align-items:center}' + '#ipmph-panel .sec-t .arrow{transition:transform .2s;font-size:12px}' + '#ipmph-panel .sec-t .arrow.open{transform:rotate(90deg)}' + '#ipmph-panel .sec-collapsible{overflow:hidden;transition:max-height .3s ease}' + '#ipmph-panel .btn{display:flex;align-items:center;justify-content:center;gap:6px;padding:10px 14px;margin:4px 0;border:none;border-radius:8px;cursor:pointer;font-size:14px;font-weight:500;width:100%;transition:all .15s}' + '#ipmph-panel .btn:active{transform:scale(.98)}' + '#ipmph-panel .btn:disabled{background:#bdbdbd;cursor:not-allowed;transform:none}' + '#ipmph-panel .btn-blue{background:#1565C0;color:#fff}' + '#ipmph-panel .btn-blue:hover{background:#0D47A1}' + '#ipmph-panel .btn-green{background:#2E7D32;color:#fff}' + '#ipmph-panel .btn-green:hover{background:#1B5E20}' + '#ipmph-panel .btn-purple{background:#7B1FA2;color:#fff}' + '#ipmph-panel .btn-purple:hover{background:#6A1B9A}' + '#ipmph-panel .btn-orange{background:#E65100;color:#fff}' + '#ipmph-panel .btn-orange:hover{background:#BF360C}' + '#ipmph-panel .count-box{text-align:center;margin:8px 0;padding:10px;background:linear-gradient(135deg,#E3F2FD,#BBDEFB);border-radius:8px}' + '#ipmph-panel .count-box .num{font-size:26px;font-weight:700;color:#1565C0}' + '#ipmph-panel .count-box .lbl{font-size:11px;color:#616161}' + '#ipmph-panel .st{margin-top:10px;padding:10px 12px;background:#F5F5F5;border-radius:8px;font-size:12px;color:#616161;line-height:1.6;word-break:break-all;white-space:pre-wrap}' + '#ipmph-panel .st.ok{background:#E8F5E9;color:#2E7D32}' + '#ipmph-panel .st.err{background:#FFEBEE;color:#C62828}' + '#ipmph-panel .input-row{margin-bottom:8px}' + '#ipmph-panel .input-row label{display:block;font-size:12px;color:#666;margin-bottom:2px}' + '#ipmph-panel .input-row input{width:100%;padding:6px 8px;border:1px solid #ddd;border-radius:4px;font-size:12px;outline:none;transition:border .2s}' + '#ipmph-panel .input-row input:focus{border-color:#1565C0}' + '#ipmph-panel .check-row{display:flex;align-items:center;margin:4px 0;font-size:12px;cursor:pointer}' + '#ipmph-panel .check-row input{margin-right:6px;accent-color:#1565C0}' ); // ===================== 工具 ===================== var questionsData = []; var isRunning = false; var stopRequested = false; function $(id) { return document.getElementById(id); } function setStatus(msg, type) { var s = $('ipmph-st'); if (s) { s.textContent = msg; s.className = 'st' + (type ? ' ' + type : ''); } } function setCount(n) { var c = $('ipmph-count'); if (c) c.textContent = n; var d = $('ipmph-dl-html'); if (d) d.disabled = n === 0; } function clean(text) { if (!text) return ''; return text.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim(); } // 保存/加载API配置 function saveApiConfig() { function val(id) { var el = $(id); return el ? el.value : ''; } function chk(id) { var el = $(id); return el ? el.checked : false; } var cfg = { url: val('ipmph-api-url'), key: val('ipmph-api-key'), model: val('ipmph-api-model'), thinking: chk('ipmph-thinking'), interval: parseInt(val('ipmph-interval'), 10) || 3, retry429: parseInt(val('ipmph-retry429'), 10) || 30 }; try { localStorage.setItem('ipmph_api_cfg', JSON.stringify(cfg)); } catch(e) {} } function loadApiConfig() { try { var raw = localStorage.getItem('ipmph_api_cfg'); if (raw) return JSON.parse(raw); } catch(e) {} return { url: 'https://api.openai.com/v1', key: '', model: 'gpt-4o-mini', thinking: false, interval: 3, retry429: 30 }; } // ===================== 检测页面类型 ===================== function isAnswerPage() { return document.querySelectorAll('.answer .right').length > 0; } function isExamAnswerPage() { return isExamPage() && document.querySelectorAll('.list_papers').length > 0; } function isExamPage() { return window.location.hostname === 'tk.ipmph.com'; } function isZengzhiPage() { return window.location.hostname === 'zengzhi.ipmph.com'; } function isBookDetailPage() { return isZengzhiPage() && ( window.location.hash.indexOf('/bookDetail') !== -1 || window.location.href.indexOf('/bookDetail') !== -1 ); } function showStopBtn(show) { var btn = $('ipmph-stop'); if (btn) btn.style.display = show ? 'flex' : 'none'; } // ===================== 从答案页面提取所有题目 ===================== function extractFromAnswerPage() { questionsData = []; var questionContainers = document.querySelectorAll('.singleChoice, .judgmentQuestion, .multiChoice, .judge'); questionContainers.forEach(function(container, idx) { var q = { id: idx + 1, type: '', content: '', options: [], answer: '', explanation: '' }; var typeEl = container.querySelector('.type span'); if (typeEl) q.type = clean(typeEl.textContent); var stemWrapper = container.querySelector('.main > .label_body .label_wrapper'); if (stemWrapper) { var stem = ''; stemWrapper.querySelectorAll('span:not(.currentNum)').forEach(function(sp) { var t = clean(sp.textContent); if (t) stem += t + ' '; }); q.content = clean(stem); } container.querySelectorAll('.anwserItem, .answerItem').forEach(function(item) { var lw = item.querySelector('.label_wrapper'); if (lw) { var opt = ''; lw.querySelectorAll('span:not(.currentNum)').forEach(function(sp) { var t = clean(sp.textContent); if (t) opt += t + ' '; }); opt = clean(opt); if (opt) { q.options.push(opt); return; } } var label = item.querySelector('.van-radio__label, .van-checkbox__label'); if (label) { var lt = clean(label.textContent); if (lt && lt.length < 200) { q.options.push(lt); return; } } }); var answerDiv = container.querySelector('.answer'); if (answerDiv) { var rightSpan = answerDiv.querySelector('.right'); if (rightSpan) q.answer = clean(rightSpan.textContent); } var analysisEl = container.querySelector('.analysis .label_wrapper'); if (analysisEl) { var exp = ''; analysisEl.querySelectorAll('span:not(.currentNum)').forEach(function(sp) { var t = clean(sp.textContent); if (t) exp += t + ' '; }); q.explanation = clean(exp); } if (q.content) questionsData.push(q); }); return questionsData.length; } // ===================== 从答题页面提取当前题目 ===================== function extractCurrentQuestion() { if (isExamPage()) return examExtractCurrentQuestion(); var q = { type: '', content: '', options: [], answer: '', explanation: '' }; var container = document.querySelector('.singleChoice, .multiChoice, .judge, .judgmentQuestion, .shortAnswer, .fillBlank'); if (!container) return null; var stemWrapper = container.querySelector('.label_body .label_wrapper'); if (stemWrapper) { var stem = ''; stemWrapper.querySelectorAll('span:not(.currentNum)').forEach(function(sp) { var t = clean(sp.textContent); if (t) stem += t + ' '; }); q.content = clean(stem); } container.querySelectorAll('.anwserItem, .answerItem').forEach(function(item) { var lw = item.querySelector('.label_wrapper'); if (lw) { var opt = ''; lw.querySelectorAll('span:not(.currentNum)').forEach(function(sp) { var t = clean(sp.textContent); if (t) opt += t + ' '; }); opt = clean(opt); if (opt) { q.options.push(opt); return; } } var label = item.querySelector('.van-radio__label, .van-checkbox__label'); if (label) { var lt = clean(label.textContent); if (lt && lt.length < 200) { q.options.push(lt); return; } } }); return q.content ? q : null; } // ===================== 页面交互 ===================== function clickNext() { if (isExamPage()) return examClickNext(); var buttons = document.querySelectorAll('.bottom .van-button'); for (var i = 0; i < buttons.length; i++) { var text = buttons[i].querySelector('.van-button__text'); if (text && clean(text.textContent).indexOf('下一题') !== -1 && !buttons[i].disabled) { buttons[i].click(); return true; } } return false; } function clickQuestionNumber(num) { var numDivs = document.querySelectorAll('.itemNumber > div'); for (var i = 0; i < numDivs.length; i++) { if (parseInt(clean(numDivs[i].textContent), 10) === num) { numDivs[i].click(); return true; } } return false; } function waitForQuestionLoad(maxWait) { if (isExamPage()) return Promise.resolve(true); // 考试页面无需等待Vue渲染 var start = Date.now(); return new Promise(function(resolve) { function check() { var c = document.querySelector('.singleChoice, .multiChoice, .judge, .judgmentQuestion, .shortAnswer, .fillBlank'); if (c && c.querySelector('.label_body')) { resolve(true); } else if (Date.now() - start < maxWait) { setTimeout(check, 100); } else { resolve(false); } } check(); }); } // 点击选项(兼容Vue2和Vue3) function clickOption(index) { if (isExamPage()) return examClickOption(index); try { var items = document.querySelectorAll('.anwserItem, .answerItem'); if (index < 0 || index >= items.length) return false; // 支持radio和checkbox var clickable = items[index].querySelector('.van-radio, .van-checkbox'); if (!clickable) return false; // Vue 2: __vue__ var vm = clickable.__vue__; // Vue 3: __vueParentComponent if (!vm && clickable.__vueParentComponent) { vm = clickable.__vueParentComponent.proxy || clickable.__vueParentComponent; } if (!vm) { var parent = clickable.parentElement; while (parent && !vm) { if (parent.__vueParentComponent) vm = parent.__vueParentComponent.proxy || parent.__vueParentComponent; if (parent.__vue__) vm = parent.__vue__; parent = parent.parentElement; } } if (vm) { // 直接通过group设置值 var group = clickable.closest('.van-radio-group, .van-checkbox-group'); if (group) { var groupVm = group.__vue__ || (group.__vueParentComponent && (group.__vueParentComponent.proxy || group.__vueParentComponent)); if (groupVm) { var val = vm.name; if (val === undefined || val === null || val === '') val = vm.value; if (val === undefined || val === null || val === '') val = String(index); console.log('[下载器] group emit:', val); // checkbox需要传数组 if (clickable.classList.contains('van-checkbox')) { var currentVal = groupVm.value || groupVm.modelValue || []; if (!Array.isArray(currentVal)) currentVal = []; if (currentVal.indexOf(val) === -1) { currentVal = currentVal.concat([val]); } if (typeof groupVm.$emit === 'function') { groupVm.$emit('input', currentVal); groupVm.$emit('change', currentVal); } else if (typeof groupVm.emit === 'function') { groupVm.emit('input', currentVal); groupVm.emit('change', currentVal); } } else { // radio直接传单个值 if (typeof groupVm.$emit === 'function') { groupVm.$emit('input', val); groupVm.$emit('change', val); } else if (typeof groupVm.emit === 'function') { groupVm.emit('input', val); groupVm.emit('change', val); } } return true; } } } // 兜底 clickable.click(); return true; } catch(e) { console.log('[下载器] clickOption错误:', e.message); return false; } } // 根据答案文本点击选项(支持单选和多选) function clickAnswerByText(answerText) { if (!answerText) return false; answerText = clean(answerText).toUpperCase(); // 判断题 if (answerText === '对' || answerText === 'TRUE' || answerText === 'T' || answerText === '√') { return clickOption(0); } if (answerText === '错' || answerText === 'FALSE' || answerText === 'F' || answerText === '×') { return clickOption(1); } // 多选题:匹配 "ABD" 或 "A、B、D" 或 "A B D" 等格式 var letters = answerText.replace(/[^A-F]/g, ''); if (letters.length >= 2) { // 多选:逐个点击 var clicked = false; for (var i = 0; i < letters.length; i++) { var idx = letters.charCodeAt(i) - 65; if (clickOption(idx)) clicked = true; } return clicked; } // 单选题 var letterMatch = answerText.match(/^([A-F])/); if (letterMatch) { idx = letterMatch[1].charCodeAt(0) - 65; return clickOption(idx); } return false; } // ===================== 考试页面(考试页面)专用函数 ===================== function isBTypeQuestion() { return document.getElementById('parentQuestion') && document.getElementById('questionList'); } function examExtractCurrentQuestion() { // B型题:返回一个子题目数组 if (isBTypeQuestion()) { var questions = []; // 共享选项 var sharedOptions = []; document.querySelectorAll('#parentContent .theFloatDiv').forEach(function(div) { var label = div.querySelector('label'); if (label) sharedOptions.push(clean(label.textContent)); }); // 题型 var typeSpan = document.querySelector('#parentQuestion span[style*="font-size:20px"]'); if (!typeSpan) typeSpan = document.querySelector('#parentQuestion span[style*="display: block"]'); var btype = typeSpan ? clean(typeSpan.textContent) : 'B型题'; // 子题目 document.querySelectorAll('#questionList > div[id]').forEach(function(div) { var span = div.querySelector('span rich-text'); if (!span) return; var content = clean(span.textContent); if (!content) return; var numMatch = content.match(/^(\d+)\./); var num = numMatch ? parseInt(numMatch[1], 10) : 0; questions.push({ id: num, type: btype, content: content, options: sharedOptions.slice(), answer: '', explanation: '' }); }); return questions.length > 0 ? questions : null; } // 普通题型(A1/A2/判断等) var q = { type: '', content: '', options: [], answer: '', explanation: '' }; var titleEl = document.querySelector('.question_title rich-text'); if (titleEl) q.content = clean(titleEl.textContent); if (!q.content) { titleEl = document.querySelector('.question_title'); if (titleEl) q.content = clean(titleEl.textContent); } typeSpan = document.querySelector('span[style*="font-size:20px"]'); if (typeSpan) q.type = clean(typeSpan.textContent); document.querySelectorAll('.theAnsDiv').forEach(function(div, oi) { var label = div.querySelector('label .font_rate, label rich-text'); if (label) { q.options.push(clean(label.textContent)); } else { var l = div.querySelector('label'); if (l) q.options.push(clean(l.textContent)); } }); return q.content ? q : null; } function examClickOption(index) { // B型题:在第一个子题目内点击 var subDivs = document.querySelectorAll('#questionList > div[id]'); if (subDivs.length > 0) { var items = subDivs[0].querySelectorAll('.xu_xiang'); if (index >= 0 && index < items.length) { items[index].click(); return true; } return false; } items = document.querySelectorAll('.theAnsDiv'); if (index >= 0 && index < items.length) { items[index].click(); return true; } return false; } function examClickNext() { var btn = document.getElementById('next'); if (btn && !btn.disabled) { btn.click(); return true; } return false; } function extractExamAnswerPage() { questionsData = []; document.querySelectorAll('.list_papers').forEach(function(paper, idx) { var q = { id: idx + 1, type: '', content: '', options: [], answer: '', explanation: '' }; // 题型 var typeEl = paper.querySelector('.list_papers_head span'); if (typeEl) q.type = clean(typeEl.textContent.replace('题型:', '')); // 题干 var stemEl = paper.querySelector('.test_paper_head rich-text'); if (stemEl) q.content = clean(stemEl.textContent); // 选项 paper.querySelectorAll('ul li').forEach(function(li) { var txt = li.querySelector('rich-text'); if (txt) q.options.push(clean(txt.textContent)); }); // 正确答案 var ansEls = paper.querySelectorAll('.answer_d rich-text'); if (ansEls.length >= 1) q.answer = clean(ansEls[0].textContent); // 解析 var expEl = paper.querySelector('.jie'); if (expEl) q.explanation = clean(expEl.textContent); if (q.content) questionsData.push(q); }); return questionsData.length; } function examParseAnswerCard() { // 考试页面一次性显示所有题,从question_title获取题号 var questions = []; document.querySelectorAll('#qGroup_end').forEach(function(el) { var num = parseInt(el.textContent, 10); if (!isNaN(num)) questions.push(num); }); return questions; } // ===================== LLM 调用 ===================== function callLLM(prompt) { return new Promise(function(resolve, reject) { var cfg = loadApiConfig(); if (!cfg.key) { reject(new Error('请先填写API Key')); return; } var apiUrl = (cfg.url || 'https://api.openai.com/v1').replace(/\/+$/, '') + '/chat/completions'; var body = { model: cfg.model || 'gpt-4o-mini', messages: [ { role: 'system', content: '你是医学考试答题助手。规则:1)单选题只回复一个大写字母(A/B/C/D/E);2)多选题回复所有正确选项的字母连写(如ABD);3)判断题只回复"对"或"错";4)不要解释、不要分析、不要其他任何文字。' }, { role: 'user', content: prompt } ], stream: false, temperature: 0.1, max_tokens: 1000 }; // 如果启用思考模式 if (cfg.thinking) { body.temperature = 0.6; body.max_tokens = 4000; } GM_xmlhttpRequest({ method: 'POST', url: apiUrl, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + cfg.key }, data: JSON.stringify(body), onload: function(response) { try { if (response.status !== 200) { reject(new Error('API返回错误 (' + response.status + '): ' + response.responseText.substring(0, 200))); return; } var data = JSON.parse(response.responseText); var content = ''; if (data.choices && data.choices.length > 0) { var choice = data.choices[0]; // 标准OpenAI格式 if (choice.message && choice.message.content) { content = choice.message.content; } // 兼容部分模型的reasoning_content字段 if (!content && choice.message && choice.message.reasoning_content) { // 从reasoning中提取最终答案 content = choice.message.reasoning_content; } // 兼容delta格式 if (!content && choice.delta && choice.delta.content) { content = choice.delta.content; } } if (!content) { reject(new Error('API返回内容为空')); return; } resolve(clean(content)); } catch(e) { reject(new Error('解析API响应失败: ' + e.message)); } }, onerror: function(err) { reject(new Error('网络请求失败: ' + (err.statusText || '未知错误'))); }, ontimeout: function() { reject(new Error('请求超时')); }, timeout: 30000 }); }); } // 从LLM回复中提取答案(支持单选和多选) function parseAnswerFromLLM(response, isJudgment) { var text = clean(response); // 判断题优先 if (isJudgment) { var lastTrue = text.lastIndexOf('对'); var lastFalse = text.lastIndexOf('错'); if (lastTrue > lastFalse && lastTrue >= 0) return '对'; if (lastFalse > lastTrue && lastFalse >= 0) return '错'; } var cleaned = text .replace(/[A-F][.、.,,]\s*/g, '') .replace(/[((][A-F][))]/g, '') .replace(/选项[::]/g, '') .replace(/答案[::]?\s*/g, '答案:'); // 匹配 "答案:ABD" 或 "答案是ABD"(支持多选) var m = cleaned.match(/答案[::是为]?\s*([A-F]{2,}|[A-F]|对|错)/); if (m) return m[1]; // 匹配 "选ABD" m = cleaned.match(/选\s*([A-F]{2,}|[A-F])/); if (m) return m[1]; // 匹配 "A、B、D" 格式 m = cleaned.match(/([A-F])\s*[、,,]\s*([A-F])(?:\s*[、,,]\s*([A-F]))?(?:\s*[、,,]\s*([A-F]))?(?:\s*[、,,]\s*([A-F]))?/); if (m) { var result = m[1] + m[2]; if (m[3]) result += m[3]; if (m[4]) result += m[4]; if (m[5]) result += m[5]; return result; } // 从末尾扫描 var lines = text.trim().split(/[\r\n]+/); for (var li = lines.length - 1; li >= 0; li--) { var line = lines[li].trim(); if (!line || /^[A-F][.、.,,]/.test(line)) continue; var endMulti = line.match(/([A-F]{2,})\s*[。.。]?\s*$/); if (endMulti) return endMulti[1]; var endSingle = line.match(/([A-F])\s*[。.。]?\s*$/); if (endSingle) return endSingle[1]; if (isJudgment) { if (/对\s*[。.。]?\s*$/.test(line)) return '对'; if (/错\s*[。.。]?\s*$/.test(line)) return '错'; } } // 兜底:从后往前找连续字母 var upperText = text.toUpperCase(); var lastLetters = ''; for (var ci = upperText.length - 1; ci >= 0; ci--) { var ch = upperText.charAt(ci); if (ch >= 'A' && ch <= 'F') { lastLetters = ch + lastLetters; } else if (lastLetters.length > 0) { break; } } if (lastLetters.length >= 2) return lastLetters; if (lastLetters.length === 1) return lastLetters; return clean(response); } // 构建题目prompt function buildPrompt(q) { var prompt = '题目:' + q.content + '\n'; if (q.options.length > 0) { q.options.forEach(function(opt, oi) { prompt += String.fromCharCode(65 + oi) + '. ' + opt + '\n'; }); } prompt += '\n请只返回答案字母(如 A、B、C、D),判断题返回"对"或"错"。'; return prompt; } // ===================== LLM自动答题 ===================== function llmAutoAnswer() { if (isRunning) return; if (isAnswerPage()) { setStatus('当前已在答案页面,请直接点击"一键提取"。', 'err'); return; } var cfg = loadApiConfig(); if (!cfg.key) { setStatus('请先填写API配置(API地址、Key、模型)。', 'err'); return; } isRunning = true; stopRequested = false; showStopBtn(true); // 考试页面:逐题答题直到没有下一题 if (isExamPage()) { setStatus('开始LLM自动答题(考试页面)...'); questionsData = []; var current = 1; var answered = 0; var totalAnswered = 0; var MAX_QUESTIONS = 200; var interval = (cfg.interval || 3) * 1000; var retry429 = (cfg.retry429 || 30) * 1000; function examProcessNext() { if (stopRequested || totalAnswered >= MAX_QUESTIONS) { setStatus('已完成。共答 ' + answered + '/' + totalAnswered + ' 题', 'ok'); isRunning = false; showStopBtn(false); return; } var result = examExtractCurrentQuestion(); if (!result) { setStatus('未找到题目元素。共答 ' + answered + '/' + totalAnswered + ' 题', 'ok'); setCount(questionsData.length); isRunning = false; showStopBtn(false); return; } // B型题:单个题干多个子题目 if (Array.isArray(result)) { var bQuestions = result; var bIdx = 0; function processBQuestion() { if (stopRequested || bIdx >= bQuestions.length) { // B型题全部完成,点击下一题 if (!examClickNext()) { setStatus('答题完成!共答 ' + answered + '/' + totalAnswered + ' 题', 'ok'); isRunning = false; showStopBtn(false); } else { setTimeout(examProcessNext, interval); } return; } var q = bQuestions[bIdx]; setStatus('正在回答 B型题 第 ' + q.id + ' 题...'); function bDoCall() { callLLM(buildPrompt(q)).then(function(response) { var answer = parseAnswerFromLLM(response, false); console.log('[下载器] B型第' + q.id + '题 LLM回复:', response, '答案:', answer); // B型题:点击对应子题目内的选项 var subDivs = document.querySelectorAll('#questionList > div[id]'); if (bIdx < subDivs.length) { var items = subDivs[bIdx].querySelectorAll('.xu_xiang'); var letterIdx = (answer.toUpperCase().charCodeAt(0) || 65) - 65; if (letterIdx >= 0 && letterIdx < items.length) { items[letterIdx].click(); answered++; q.answer = answer; } } questionsData.push(q); totalAnswered++; setCount(questionsData.length); bIdx++; setTimeout(processBQuestion, interval); }).catch(function(err) { var msg = err.message || ''; if (msg.indexOf('429') !== -1 || msg.indexOf('Too Many') !== -1 || msg.indexOf('Rate') !== -1) { setStatus('429 频率限制,' + (retry429 / 1000) + '秒后重试...', 'err'); setTimeout(function() { bDoCall(); }, retry429); } else { setStatus('B型第' + q.id + '题失败: ' + msg, 'err'); bIdx++; setTimeout(processBQuestion, interval); } }); } bDoCall(); } processBQuestion(); return; } // 普通题型 var q = result; setStatus('正在回答第 ' + current + ' 题...'); var isJud = (q.type || '').indexOf('判断') !== -1; callLLM(buildPrompt(q)).then(function(response) { var answer = parseAnswerFromLLM(response, isJud); console.log('[下载器] 第' + current + '题 LLM回复:', response, '答案:', answer); var clicked = clickAnswerByText(answer); if (clicked) { answered++; q.answer = answer; } q.id = current; questionsData.push(q); totalAnswered++; current++; setCount(questionsData.length); if (!examClickNext()) { setStatus('答题完成!共答 ' + answered + '/' + totalAnswered + ' 题', 'ok'); isRunning = false; showStopBtn(false); } else { setTimeout(examProcessNext, interval); } }).catch(function(err) { var msg = err.message || ''; if (msg.indexOf('429') !== -1 || msg.indexOf('Too Many') !== -1 || msg.indexOf('Rate') !== -1) { setStatus('429 频率限制,' + (retry429 / 1000) + '秒后自动重试...', 'err'); setTimeout(function() { examProcessNext(); }, retry429); } else { setStatus('第 ' + current + ' 题失败: ' + msg + '\n已停止答题,共答 ' + answered + '/' + totalAnswered + ' 题', 'err'); setCount(questionsData.length); isRunning = false; showStopBtn(false); } }); } setTimeout(examProcessNext, 500); return; } // 增值服务页面原有逻辑 setStatus('正在解析答题卡...'); var types = parseAnswerCard(); if (types.length === 0) { setStatus('未找到答题卡。', 'err'); isRunning = false; showStopBtn(false); return; } var totalQuestions = 0; var typeMap = new Map(); types.forEach(function(t) { t.numbers.forEach(function(n) { typeMap.set(n, t.name); totalQuestions = Math.max(totalQuestions, n); }); }); setStatus('开始LLM自动答题:共 ' + totalQuestions + ' 题\n正在回答第 1 题...'); clickQuestionNumber(1); var i = 1; answered = 0; var errors = []; function processNext() { if (stopRequested) { setStatus('已停止。已回答 ' + answered + '/' + (i - 1) + ' 题', 'err'); isRunning = false; showStopBtn(false); return; } if (i > totalQuestions) { var msg = 'LLM答题完成!成功 ' + answered + '/' + totalQuestions + ' 题'; if (errors.length > 0) msg += '\n失败: ' + errors.join(', '); msg += '\n请检查答案后手动交卷'; setStatus(msg, answered > 0 ? 'ok' : 'err'); isRunning = false; showStopBtn(false); return; } var current = i; setStatus('正在回答第 ' + current + '/' + totalQuestions + ' 题...'); waitForQuestionLoad(2000).then(function() { setTimeout(function() { if (stopRequested) { setStatus('已停止。已回答 ' + answered + '/' + (current - 1) + ' 题', 'err'); isRunning = false; showStopBtn(false); return; } var q = extractCurrentQuestion(); if (!q) { errors.push(current); i++; gotoNext(current, totalQuestions); return; } var isJud = q.type.indexOf('判断') !== -1 || document.querySelector('.judgmentQuestion'); var prompt = buildPrompt(q); callLLM(prompt).then(function(response) { var answer = parseAnswerFromLLM(response, isJud); console.log('[下载器] 第' + current + '题 LLM原始回复:', response); console.log('[下载器] 第' + current + '题 解析答案:', answer); var clicked = clickAnswerByText(answer); var log = '第 ' + current + ' 题\nLLM回复: ' + response.substring(0, 80) + '\n解析答案: ' + answer + '\n点击: ' + (clicked ? '成功' : '失败'); if (clicked) { answered++; setStatus(log, 'ok'); } else { errors.push(current); setStatus(log, 'err'); } i++; gotoNext(current, totalQuestions); }).catch(function(err) { var msg = err.message || ''; if (msg.indexOf('429') !== -1 || msg.indexOf('Too Many') !== -1 || msg.indexOf('Rate') !== -1) { var r = (loadApiConfig().retry429 || 30); setStatus('429 频率限制,' + r + '秒后自动重试第 ' + current + ' 题...'); setTimeout(function() { errors.push(current); i++; gotoNext(current, totalQuestions); }, r * 1000); } else { errors.push(current); setStatus('第 ' + current + ' 题LLM调用失败: ' + msg); i++; gotoNext(current, totalQuestions); } }); }, 300); }); } function gotoNext(current, total) { if (current < total) { if (!clickNext()) clickQuestionNumber(current + 1); } var iv = (loadApiConfig().interval || 3) * 1000; setTimeout(processNext, iv); } setTimeout(processNext, 500); } // ===================== LLM自动答题并交卷 ===================== function llmAnswerAndSubmit() { if (isRunning) return; if (isAnswerPage()) { scanAllQuestions(); return; } // 考试页面:与LLM答题相同(无交卷概念) if (isExamPage()) { llmAutoAnswer(); return; } var cfg = loadApiConfig(); if (!cfg.key) { setStatus('请先填写API配置(API地址、Key、模型)。', 'err'); return; } isRunning = true; stopRequested = false; showStopBtn(true); setStatus('正在解析答题卡...'); var types = parseAnswerCard(); if (types.length === 0) { setStatus('未找到答题卡。', 'err'); isRunning = false; showStopBtn(false); return; } var totalQuestions = 0; var typeMap = new Map(); types.forEach(function(t) { t.numbers.forEach(function(n) { typeMap.set(n, t.name); totalQuestions = Math.max(totalQuestions, n); }); }); setStatus('开始LLM自动答题:共 ' + totalQuestions + ' 题'); clickQuestionNumber(1); var i = 1; var answered = 0; function processNext() { if (stopRequested) { setStatus('已停止。已回答 ' + answered + '/' + (i - 1) + ' 题', 'err'); isRunning = false; showStopBtn(false); return; } if (i > totalQuestions) { setStatus('答题完成!' + answered + '/' + totalQuestions + ' 题已作答\n正在交卷...'); setTimeout(function() { doSubmit(); }, 500); return; } var current = i; setStatus('正在回答第 ' + current + '/' + totalQuestions + ' 题...'); waitForQuestionLoad(2000).then(function() { setTimeout(function() { if (stopRequested) { setStatus('已停止。已回答 ' + answered + '/' + (current - 1) + ' 题', 'err'); isRunning = false; showStopBtn(false); return; } var q = extractCurrentQuestion(); if (!q) { i++; gotoNext(current, totalQuestions); return; } var isJud = q.type.indexOf('判断') !== -1 || document.querySelector('.judgmentQuestion'); var prompt = buildPrompt(q); callLLM(prompt).then(function(response) { var answer = parseAnswerFromLLM(response, isJud); if (clickAnswerByText(answer)) answered++; i++; gotoNext(current, totalQuestions); }).catch(function() { i++; gotoNext(current, totalQuestions); }); }, 300); }); } function gotoNext(current, total) { if (current < total) { if (!clickNext()) clickQuestionNumber(current + 1); } var iv = (loadApiConfig().interval || 3) * 1000; setTimeout(processNext, iv); } function doSubmit() { var submitBtn = null; document.querySelectorAll('.van-button').forEach(function(btn) { var text = btn.querySelector('.van-button__text'); if (text && clean(text.textContent) === '交卷') submitBtn = btn; }); if (!submitBtn) { setStatus('未找到交卷按钮。', 'err'); isRunning = false; showStopBtn(false); return; } submitBtn.click(); setTimeout(function() { document.querySelectorAll('.el-button--primary').forEach(function(btn) { if (btn.textContent.indexOf('提交试卷') !== -1) btn.click(); }); var waitStart = Date.now(); function checkAnswer() { if (isAnswerPage()) { setTimeout(function() { var count = extractFromAnswerPage(); setCount(count); setStatus('交卷成功!共提取 ' + count + ' 道题目(含答案和解析)\n点击下方按钮下载', 'ok'); isRunning = false; showStopBtn(false); }, 500); } else if (Date.now() - waitStart < 15000) { setTimeout(checkAnswer, 500); } else { setStatus('等待答案页面超时。', 'err'); isRunning = false; showStopBtn(false); } } setTimeout(checkAnswer, 1000); }, 500); } setTimeout(processNext, 500); } // ===================== 交卷并提取 ===================== function submitAndExtract() { if (isRunning) return; if (isAnswerPage()) { scanAllQuestions(); return; } if (isExamPage()) { setStatus('考试页面无交卷功能,请使用"LLM自动答题"。', 'err'); return; } var submitBtn = null; document.querySelectorAll('.van-button').forEach(function(btn) { var text = btn.querySelector('.van-button__text'); if (text && clean(text.textContent) === '交卷') submitBtn = btn; }); if (!submitBtn) { setStatus('未找到交卷按钮。', 'err'); return; } isRunning = true; setStatus('正在交卷...'); submitBtn.click(); setTimeout(function() { document.querySelectorAll('.el-button--primary').forEach(function(btn) { if (btn.textContent.indexOf('提交试卷') !== -1) btn.click(); }); setStatus('正在等待答案页面加载...'); var waitStart = Date.now(); function checkAnswerPage() { if (isAnswerPage()) { setTimeout(function() { var count = extractFromAnswerPage(); setCount(count); if (count > 0) { setStatus('交卷成功!共提取 ' + count + ' 道题目(含答案和解析)\n点击下方按钮下载', 'ok'); } else { setStatus('交卷成功但未能提取题目。', 'err'); } isRunning = false; }, 500); } else if (Date.now() - waitStart < 15000) { setTimeout(checkAnswerPage, 500); } else { setStatus('等待答案页面超时。请手动交卷后点击"一键提取"。', 'err'); isRunning = false; } } setTimeout(checkAnswerPage, 1000); }, 500); } // ===================== 主提取逻辑 ===================== function scanAllQuestions() { if (isRunning) return; // 考试答案页面 if (isExamAnswerPage()) { var count = extractExamAnswerPage(); setCount(count); setStatus('从考试答案页面提取完成!共 ' + count + ' 道题目(含答案和解析)\n点击下方按钮下载', 'ok'); return; } // 考试页面:提取当前题目(逐题翻页由LLM自动完成) if (isExamPage()) { var result = examExtractCurrentQuestion(); if (Array.isArray(result)) { questionsData = result; setCount(result.length); setStatus('已提取当前B型题的 ' + result.length + ' 道子题目。\n考试页面请使用"LLM自动答题"逐题提取全部。', 'ok'); } else if (result) { result.id = 1; questionsData = [result]; setCount(1); setStatus('已提取当前第1道题目。\n考试页面请使用"LLM自动答题"逐题提取全部。', 'ok'); } else { setStatus('未找到题目元素。', 'err'); } return; } // 增值服务答案页面 if (isAnswerPage() && !isBookDetailPage()) { setCount(count); if (count > 0) { setStatus('从答案页面提取完成!共 ' + count + ' 道题目(含答案和解析)\n点击下方按钮下载', 'ok'); } else { setStatus('答案页面未找到题目。', 'err'); } return; } isRunning = true; setStatus('正在解析答题卡...'); var types = parseAnswerCard(); if (types.length === 0) { setStatus('未找到答题卡。', 'err'); isRunning = false; return; } var totalQuestions = 0; var typeMap = new Map(); types.forEach(function(t) { t.numbers.forEach(function(n) { typeMap.set(n, t.name); totalQuestions = Math.max(totalQuestions, n); }); }); setStatus('答题卡解析完成:共 ' + totalQuestions + ' 题\n正在逐题提取...'); questionsData = []; clickQuestionNumber(1); var i = 1; function processNext() { if (i > totalQuestions) { setCount(questionsData.length); if (questionsData.length > 0) { setStatus('提取完成!共 ' + questionsData.length + ' 道题目\n提示:交卷后在答案页面运行可同时获取答案和解析', 'ok'); } else { setStatus('未能提取到任何题目。', 'err'); } isRunning = false; return; } var current = i; setStatus('正在提取第 ' + current + '/' + totalQuestions + ' 题...'); waitForQuestionLoad(2000).then(function() { setTimeout(function() { var q = extractCurrentQuestion(); if (q) { q.id = current; q.type = typeMap.get(current) || ''; questionsData.push(q); } if (current < totalQuestions) { if (!clickNext()) clickQuestionNumber(current + 1); } i++; setTimeout(processNext, 800); }, 200); }); } setTimeout(processNext, 800); } function parseAnswerCard() { var types = []; document.querySelectorAll('.tabCard .van-collapse-item').forEach(function(item) { var titleEl = item.querySelector('.van-collapse-item__title .van-cell__title div'); if (!titleEl) return; var typeName = ''; for (var ci = 0; ci < titleEl.childNodes.length; ci++) { var child = titleEl.childNodes[ci]; if (child.nodeType === Node.TEXT_NODE) { var t = clean(child.textContent); if (t) typeName += t; } } typeName = clean(typeName); if (!typeName) return; var numbers = []; item.querySelectorAll('.itemNumber > div').forEach(function(numDiv) { var num = parseInt(clean(numDiv.textContent), 10); if (!isNaN(num)) numbers.push(num); }); if (numbers.length > 0) types.push({ name: typeName, numbers: numbers }); }); return types; } // ===================== 生成HTML ===================== function escapeHtml(text) { if (!text) return ''; return text.replace(/&/g, '&').replace(//g, '>'); } function getBreadcrumbTitle() { // 1. 尝试从面包屑获取(答案页面应有完整6级面包屑) var items = document.querySelectorAll('.el-breadcrumb__item'); var parts = []; for (var i = 0; i < items.length; i++) { var inner = items[i].querySelector('.el-breadcrumb__inner'); if (!inner) continue; var span = inner.querySelector('span'); var text = span ? clean(span.textContent) : clean(inner.textContent); if (text) parts.push(text); } console.log('[下载器] 面包屑解析结果:', parts); if (parts.length >= 2) { return parts[parts.length - 2] + ' - ' + parts[parts.length - 1]; } if (parts.length === 1) { return parts[0]; } // 2. 面包屑不足,尝试从 sessionStorage 获取批量保存时传入的试题名 var examName = sessionStorage.getItem('xuanhu_exam_name'); if (examName) { var bookName = sessionStorage.getItem('xuanhu_book_name'); if (bookName) { return bookName + ' - ' + examName; } if (parts.length >= 1) { return parts[parts.length - 1] + ' - ' + examName; } return examName; } // 3. 尝试 .examName 元素(答题界面内嵌页) var examNameEl = document.querySelector('.examName'); if (examNameEl) { var en = clean(examNameEl.textContent); if (en) return en; } // 4. 最终回退 console.log('[下载器] 面包屑和 sessionStorage 均失败,回退 document.title:', document.title); return document.title || '人卫智网练习题'; } function generateHtml(onComplete) { if (!questionsData.length) { setStatus('没有可导出的题目数据', 'err'); if (onComplete) onComplete(); return; } var title = getBreadcrumbTitle(); var customName = $('ipmph-filename') ? clean($('ipmph-filename').value) : ''; // 批量模式且勾选"自动获取试卷名称"时,强制使用面包屑名称,忽略输入框 var autonameCb = $('ipmph-batch-autoname'); var useAutoName = onComplete && autonameCb && autonameCb.checked; var fileName = useAutoName ? title : (customName || title); var now = new Date().toLocaleString('zh-CN'); var hasAnswer = questionsData.some(function(q) { return q.answer; }); var css = '*{margin:0;padding:0;box-sizing:border-box}' + 'body{font-family:"PingFang SC","Microsoft YaHei",sans-serif;background:#f5f5f5;color:#333;padding:20px}' + '.container{max-width:800px;margin:0 auto;background:#fff;border-radius:12px;box-shadow:0 2px 12px rgba(0,0,0,.08);padding:40px}' + '.title{text-align:center;font-size:24px;font-weight:700;color:#1565C0;margin-bottom:8px}' + '.meta{text-align:center;font-size:13px;color:#999;margin-bottom:30px}' + '.type-h{font-size:18px;font-weight:700;color:#1565C0;margin:30px 0 16px;padding-bottom:8px;border-bottom:2px solid #E3F2FD}' + '.q{margin-bottom:24px;padding:16px 20px;background:#FAFAFA;border-radius:8px;border-left:4px solid #1565C0}' + '.stem{font-size:16px;font-weight:600;line-height:1.8;margin-bottom:10px}' + '.stem .n{color:#1565C0}' + '.opts{margin-left:20px;margin-bottom:8px}' + '.opt{font-size:15px;line-height:2;color:#555}' + '.opt .l{display:inline-block;width:24px;font-weight:600;color:#1565C0}' + '.ans{margin-top:8px;font-size:14px;color:#1565C0;font-weight:600}' + '.exp{margin-top:4px;font-size:14px;color:#388E3C;line-height:1.6}' + '.foot{text-align:center;margin-top:30px;padding-top:20px;border-top:1px solid #e0e0e0;font-size:12px;color:#bbb}' + '@media print{body{background:#fff;padding:0}.container{box-shadow:none;padding:20px}}'; var html = '\n\n
\n\n\n