// ==UserScript== // @name 悬壶自动刷 // @namespace http://tampermonkey.net/ // @version 1.1.0 // @description 人卫智网增值服务练习题批量获取与LLM自动答题工具 // @author WorkBuddy // @match https://zengzhi.ipmph.com/answer/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @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() { var cfg = { url: ($('ipmph-api-url') || {}).value || '', key: ($('ipmph-api-key') || {}).value || '', model: ($('ipmph-api-model') || {}).value || '', thinking: ($('ipmph-thinking') || {}).checked || false }; 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 }; } // ===================== 检测页面类型 ===================== function isAnswerPage() { return document.querySelectorAll('.answer .right').length > 0; } 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() { 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() { 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) { 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) { 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) { var idx = letterMatch[1].charCodeAt(0) - 65; return clickOption(idx); } return false; } // ===================== 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); 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; var 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) { errors.push(current); setStatus('第 ' + current + ' 题LLM调用失败: ' + err.message); i++; gotoNext(current, totalQuestions); }); }, 300); }); } function gotoNext(current, total) { if (current < total) { if (!clickNext()) clickQuestionNumber(current + 1); } setTimeout(processNext, 1000); } setTimeout(processNext, 500); } // ===================== LLM自动答题并交卷 ===================== function llmAnswerAndSubmit() { if (isRunning) return; if (isAnswerPage()) { scanAllQuestions(); 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); } setTimeout(processNext, 1000); } 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; } 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 (isAnswerPage()) { var count = extractFromAnswerPage(); 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 generateHtml() { if (!questionsData.length) { setStatus('没有可导出的题目数据', 'err'); return; } var title = document.title || '人卫智网练习题'; var customName = $('ipmph-filename') ? clean($('ipmph-filename').value) : ''; var fileName = 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' + escapeHtml(title) + '\n\n\n\n
\n'; html += '
' + escapeHtml(title) + '
\n'; html += '
导出时间: ' + now + ' | 共 ' + questionsData.length + ' 题' + (hasAnswer ? ' | 含答案和解析' : '') + '
\n'; var currentType = ''; questionsData.forEach(function(q, idx) { if (q.type && q.type !== currentType) { currentType = q.type; html += '
' + escapeHtml(currentType) + '
\n'; } html += '
\n'; html += '
' + (idx + 1) + '. ' + escapeHtml(q.content); if (q.options.length > 0) html += '( )'; html += '
\n'; if (q.options.length > 0) { html += '
\n'; q.options.forEach(function(opt, oi) { html += '
' + String.fromCharCode(65 + oi) + '.' + escapeHtml(opt) + '
\n'; }); html += '
\n'; } if (q.answer) html += '
【答案】' + escapeHtml(q.answer) + '
\n'; if (q.explanation) html += '
【解析】' + escapeHtml(q.explanation) + '
\n'; html += '
\n'; }); html += '
由人卫题目下载器生成
\n
\n\n'; var blob = new Blob([html], { type: 'text/html;charset=utf-8' }); var fn = fileName.replace(/[<>:"/\\|?*]/g, '_') + '.html'; var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = fn; document.body.appendChild(a); a.click(); setTimeout(function() { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100); setStatus('文件 "' + fn + '" 下载完成!\n可在浏览器中打开后按 Ctrl+P 打印为PDF', 'ok'); } // ===================== 创建面板 ===================== function createPanel() { var cfg = loadApiConfig(); var panel = document.createElement('div'); panel.id = 'ipmph-panel'; panel.innerHTML = '
' + '

📚 人卫题目下载器

' + '
' + '' + '' + '
' + '
' + '
' + // 提取题目 '
' + '
🔍 提取题目
' + '' + '' + '
0
已识别题目数
' + '
' + // LLM自动答题 '
' + '
🤖 LLM自动答题
' + '
' + '
' + '
' + '
' + '' + '' + '' + '' + '
' + '
' + // 文件名称 '
' + '
📝 文件名称
' + '' + '
' + // 导出 '
' + '
📥 导出
' + '' + '
' + '
支持手动提取、交卷提取、LLM自动答题三种模式
' + '
'; document.body.appendChild(panel); // 拖拽 var dragging = false, dx, dy; $('ipmph-drag').addEventListener('mousedown', function(e) { if (e.target.closest('button')) return; dragging = true; dx = e.clientX - panel.getBoundingClientRect().left; dy = e.clientY - panel.getBoundingClientRect().top; panel.style.transition = 'none'; }); document.addEventListener('mousemove', function(e) { if (dragging) { panel.style.left = (e.clientX - dx) + 'px'; panel.style.top = (e.clientY - dy) + 'px'; panel.style.right = 'auto'; } }); document.addEventListener('mouseup', function() { dragging = false; panel.style.transition = ''; }); // 折叠/关闭 $('ipmph-collapse').addEventListener('click', function() { panel.classList.toggle('collapsed'); $('ipmph-collapse').textContent = panel.classList.contains('collapsed') ? '+' : '−'; }); $('ipmph-close').addEventListener('click', function() { panel.style.display = 'none'; }); // LLM配置折叠 $('ipmph-llm-toggle').addEventListener('click', function() { var body = $('ipmph-llm-body'); var arrow = $('ipmph-llm-arrow'); if (body.style.maxHeight === '0px') { body.style.maxHeight = '500px'; arrow.classList.add('open'); } else { body.style.maxHeight = '0px'; arrow.classList.remove('open'); } }); // 保存API配置 ['ipmph-api-url', 'ipmph-api-key', 'ipmph-api-model', 'ipmph-thinking'].forEach(function(id) { var el = $(id); if (el) { el.addEventListener('change', saveApiConfig); el.addEventListener('input', saveApiConfig); } }); // 按钮事件 $('ipmph-scan').addEventListener('click', scanAllQuestions); $('ipmph-submit').addEventListener('click', submitAndExtract); $('ipmph-llm-answer').addEventListener('click', llmAutoAnswer); $('ipmph-llm-submit').addEventListener('click', llmAnswerAndSubmit); $('ipmph-stop').addEventListener('click', function() { stopRequested = true; setStatus('正在停止...', 'err'); }); $('ipmph-dl-html').addEventListener('click', generateHtml); // 默认文件名 var fnInput = $('ipmph-filename'); if (fnInput && !fnInput.value) fnInput.value = document.title || '人卫智网练习题'; } // ===================== 启动 ===================== function boot() { if (document.body) { createPanel(); } else { requestAnimationFrame(boot); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function() { setTimeout(boot, 500); }); } else { setTimeout(boot, 500); } })();