// ==UserScript== // @name 常驻题库 · 手动提取版 // @namespace https://bbs.tampermonkey.net.cn/ // @version 24.0 // @description 提取答案改为手动点击,AI答题,拖拽最小化,多格式导入导出 // @author AI // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @connect * // ==/UserScript== (function() { 'use strict'; // 题库 let answerBank; try { answerBank = GM_getValue('answerBank_v16', null); } catch(e) { answerBank = null; } if (!answerBank || typeof answerBank !== 'object') { answerBank = {}; GM_setValue('answerBank_v16', answerBank); } // AI设置 let aiSettings; try { aiSettings = GM_getValue('aiSettings_v4', null); } catch(e) { aiSettings = null; } if (!aiSettings || typeof aiSettings !== 'object') { aiSettings = { apiKey: '', apiUrl: 'https://apihub.agnes-ai.com/v1', model: 'gpt-3.5-turbo', systemPrompt: '你是一个答题助手,根据题目和选项给出正确答案。请只返回一个JSON对象:{"letter": "正确答案字母", "answer": "完整答案文字"}。如果多选题,字母连写如"AD",答案文字用逗号分隔。' }; GM_setValue('aiSettings_v4', aiSettings); } let currentMode = 'auto'; let lastQuestionText = ''; let lastAnswerInfo = null; let minimized = false; let aiCalling = false; function saveBank() { GM_setValue('answerBank_v16', answerBank); } function saveAISettings() { GM_setValue('aiSettings_v4', aiSettings); } // UI容器 const floatBox = document.createElement('div'); floatBox.id = 'smartAnswerWidget'; floatBox.style.cssText = ` position: fixed; left: auto; right: 20px; top: auto; bottom: 20px; width: 520px; max-height: 640px; background: rgba(20,20,30,0.96); backdrop-filter: blur(20px); border: 1px solid rgba(255,255,255,0.15); border-radius: 16px; color: #e0e0e0; font-family: system-ui; font-size: 14px; z-index: 99999; overflow: hidden; box-shadow: 0 12px 30px rgba(0,0,0,0.5); `; document.body.appendChild(floatBox); const miniBtn = document.createElement('div'); miniBtn.id = 'smartWidgetMini'; miniBtn.style.cssText = ` position: fixed; bottom: 20px; right: 20px; width: 48px; height: 48px; background: rgba(20,20,30,0.9); border-radius: 50%; color:#fff; font-size:24px; display:none; align-items:center; justify-content:center; cursor:pointer; z-index:100000; box-shadow:0 4px 15px rgba(0,0,0,0.4); `; miniBtn.innerHTML = '📚'; document.body.appendChild(miniBtn); floatBox.style.display = 'block'; // 拖拽 let isDragging = false, dragStartX, dragStartY, startLeft, startTop; function enableDrag(h) { if(!h) return; h.style.cursor='move'; h.addEventListener('mousedown', e => { if(e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; isDragging=true; const r = floatBox.getBoundingClientRect(); startLeft=r.left; startTop=r.top; dragStartX=e.clientX; dragStartY=e.clientY; floatBox.style.transition='none'; e.preventDefault(); }); } document.addEventListener('mousemove', e => { if(!isDragging) return; let l = startLeft + e.clientX - dragStartX, t = startTop + e.clientY - dragStartY; l = Math.min(Math.max(l,10), window.innerWidth-floatBox.offsetWidth-10); t = Math.min(Math.max(t,10), window.innerHeight-floatBox.offsetHeight-10); floatBox.style.left=l+'px'; floatBox.style.top=t+'px'; floatBox.style.right='auto'; floatBox.style.bottom='auto'; }); document.addEventListener('mouseup', ()=> isDragging=false); function renderHeader() { return `
`; } // 题目检测 function detectQuestionText() { const sels = [ '[data-cangjie-leaf-block="true"]', '.question-content', '.stem', '.exam-question-title', '.question-title', '.que_title', '.timu', '.subject-question', '.q-title', '.q-content', '.problem' ]; for(let s of sels){ const el = document.querySelector(s); if(el && el.innerText.trim()) return el.innerText.trim(); } return ''; } function isResultPage() { return /正确选项|回答正确|回答错误|试题解析|正确答案/.test(document.body.innerText); } function extractAnswerFromPage() { let letter='', full=''; const body = document.body.innerText; const m = body.match(/(?:正确选项|正确答案|标准答案)[\s::]*([A-D]+)/) || body.match(/答案[\s::]*([A-D]+)/); if(m) letter=m[1]; else return {letter:'',fullText:''}; const all = document.querySelectorAll('body *'); const re = new RegExp(`^${letter}[.、.)\\s]`); for(let el of all){ if(el.offsetParent===null && el.tagName!=='BODY') continue; if(['SCRIPT','STYLE'].includes(el.tagName)) continue; const t = (el.innerText||el.textContent).trim(); if(t.length<3) continue; if(re.test(t)){ full = t.replace(/^[A-D][.、.)\\s]*/,'').trim(); break; } } return {letter, fullText:full}; } function getCurrentOptions() { const options = []; const optionEls = document.querySelectorAll('.exam-question-option, .option-item, .stem-options label, [class*="option"], li'); for(let el of optionEls){ const text = (el.innerText||el.textContent).trim(); if(/^[A-D][.、.]\s/.test(text)){ const letter = text.charAt(0); const content = text.replace(/^[A-D][.、.]\s*/, '').trim(); options.push({letter, content}); } } return options; } function matchAnswer(qText) { if(!qText) return null; for(const [key,val] of Object.entries(answerBank)){ if(qText.includes(key)){ const [letter,full] = val.includes('|') ? val.split('|') : [val,'']; const isMulti = letter.length>1; let options = []; if(isMulti && full){ const parts = full.split(/[,,、]/).map(s=>s.trim()).filter(s=>s); if(parts.length === letter.length){ for(let i=0;i`
${o.letter}${o.text}
`).join(''); ansHtml = `
${items}
`; } else { ansHtml = `
${info.letter}${info.fullAnswer?`
${info.fullAnswer}
`:''}
`; } } let aiBtnHtml = ''; if(lastQuestionText && !info?.found && !isResultPage()){ aiBtnHtml = ``; } return `
📖 自动识别${info?.found?'✅已匹配':(lastQuestionText?'⚠️未匹配':'⏳等待')}
${lastQuestionText?`
${lastQuestionText}
`:'
等待题目...
'} ${ansHtml} ${aiBtnHtml}
题库 ${Object.keys(answerBank).length} 题
`; } // 【修改】提取模式:不再自动填入字母和完整答案,初始为空,手动点击提取 function renderExtractMode(){ const qText = lastQuestionText || detectQuestionText(); return `
📥 提取录入
${isResultPage()?'✅ 答案页面(点击下方按钮提取)':'⚠️ 非答案页面(请手动填写答案)'}
`; } function renderManageMode(){ const entries = Object.entries(answerBank); const table = entries.length ? ` ${entries.map(([k,v],i)=>{ const [l,f]=v.includes('|')?v.split('|'):[v,'']; return ``; }).join('')}
#关键字答案操作
${i+1}${k.substring(0,20)}${l}
` : '
📭 题库为空
'; return `
⚙️ 题库管理 (${entries.length}题)
${table}
`; } function toggleMinimize() { minimized=!minimized; floatBox.style.display=minimized?'none':'block'; miniBtn.style.display=minimized?'flex':'none'; if(!minimized) refreshWidget(); } miniBtn.onclick = toggleMinimize; function forceShowWidget(){ minimized=false; floatBox.style.display='block'; miniBtn.style.display='none'; } function refreshWidget(){ if(minimized) return; let c = currentMode==='auto'?renderAutoMode(): currentMode==='extract'?renderExtractMode(): renderManageMode(); floatBox.innerHTML = renderHeader() + c; bindEvents(); enableDrag(document.getElementById('widgetDragHandle')); } // AI相关 function getFullApiUrl() { let url = aiSettings.apiUrl.trim(); if (!url.endsWith('/chat/completions')) { url = url.replace(/\/$/, ''); url += '/chat/completions'; } return url; } async function callAI(questionText, options) { if(!aiSettings.apiKey){ return { error: '请先在【管理】->【AI设置】中配置API密钥。' }; } let userContent = `题目:${questionText}`; if(options && options.length > 0){ const optionStr = options.map(o => `${o.letter}. ${o.content}`).join('\n'); userContent += `\n选项:\n${optionStr}`; } const messages = [ { role: 'system', content: aiSettings.systemPrompt }, { role: 'user', content: userContent } ]; const requestBody = { model: aiSettings.model, messages: messages, temperature: 0.3, max_tokens: 200 }; const fullUrl = getFullApiUrl(); console.log('[AI] 请求地址:', fullUrl); try { const response = await fetch(fullUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${aiSettings.apiKey}` }, body: JSON.stringify(requestBody) }); if(!response.ok){ const errText = await response.text(); return { error: `HTTP ${response.status}: ${errText.substring(0, 150)}` }; } const data = await response.json(); if(data.error) return { error: `API错误: ${data.error.message || JSON.stringify(data.error)}` }; const content = data.choices[0].message.content.trim(); let result; try { const cleanContent = content.replace(/```json|```/g, '').trim(); result = JSON.parse(cleanContent); } catch(e) { const letterMatch = content.match(/[A-D]+/); if(letterMatch) result = { letter: letterMatch[0], answer: content }; else result = { letter: '', answer: content }; } return result; } catch(e) { return { error: `网络错误: ${e.message}` }; } } async function handleAIAnswer() { if(aiCalling) return; const question = lastQuestionText; if(!question) return; aiCalling = true; refreshWidget(); const options = getCurrentOptions(); const result = await callAI(question, options); aiCalling = false; if(result.error){ document.getElementById('aiMsg').innerText = '❌ ' + result.error; } else if(result && result.letter){ const key = question.substring(0, 30); const letter = result.letter.toUpperCase(); const full = result.answer || ''; answerBank[key] = full ? `${letter}|${full}` : letter; saveBank(); lastAnswerInfo = matchAnswer(question); document.getElementById('aiMsg').innerText = '✅ AI解答已保存'; } else { document.getElementById('aiMsg').innerText = '❌ AI未返回有效答案'; } refreshWidget(); } async function testAIConnection() { const testBtn = document.getElementById('testAIBtn'); testBtn.disabled = true; testBtn.innerText = '测试中...'; const result = await callAI('1+1=?', [{letter:'A', content:'2'}, {letter:'B', content:'3'}]); testBtn.disabled = false; testBtn.innerText = '测试连接'; if(result.error){ alert('连接失败:' + result.error + '\n\n请检查API地址、密钥和模型名称。'); } else { alert('连接成功!AI返回:' + JSON.stringify(result)); } } function showAISettings() { const overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7); z-index:100001; display:flex; align-items:center; justify-content:center;'; overlay.innerHTML = `

🤖 AI 答题设置

自动补全 /chat/completions
`; document.body.appendChild(overlay); document.getElementById('testAIBtn').addEventListener('click', testAIConnection); document.getElementById('saveAISettingsBtn').onclick = () => { aiSettings.apiKey = document.getElementById('aiApiKey').value.trim(); aiSettings.apiUrl = document.getElementById('aiApiUrl').value.trim(); aiSettings.model = document.getElementById('aiModel').value.trim(); aiSettings.systemPrompt = document.getElementById('aiPrompt').value.trim(); saveAISettings(); document.body.removeChild(overlay); alert('AI设置已保存。'); }; document.getElementById('cancelAISettingsBtn').onclick = () => document.body.removeChild(overlay); overlay.onclick = e => { if(e.target === overlay) document.body.removeChild(overlay); }; } // 事件绑定 function bindEvents(){ document.querySelectorAll('.mode-btn').forEach(b=>b.onclick=()=>{ currentMode=b.dataset.mode; refreshWidget(); }); document.getElementById('minimizeBtn').onclick = toggleMinimize; document.getElementById('closeWidgetBtn').onclick = toggleMinimize; const saveBtn = document.getElementById('saveExtractBtn'); if(saveBtn) saveBtn.onclick = ()=>{ const k = document.getElementById('extKey').value.trim(); const l = document.getElementById('extLetter').value.trim().toUpperCase(); const f = document.getElementById('extFull').value.trim(); const msg = document.getElementById('extMsg'); if(k&&l){ answerBank[k] = f?`${l}|${f}`:l; saveBank(); msg.innerText='✅ 保存成功'; setTimeout(()=>msg.innerText='',2000); } else msg.innerText='⚠️ 请填写关键字和答案'; }; // 【新增】手动提取按钮 const manualBtn = document.getElementById('manualExtractBtn'); if(manualBtn){ manualBtn.addEventListener('click', ()=>{ if(isResultPage()){ const {letter, fullText} = extractAnswerFromPage(); document.getElementById('extLetter').value = letter; document.getElementById('extFull').value = fullText; document.getElementById('extMsg').innerText = '✅ 答案已提取'; } else { document.getElementById('extMsg').innerText = '⚠️ 当前非答案页面'; } }); } const aiBtn = document.getElementById('aiAnswerBtn'); if(aiBtn) aiBtn.onclick = handleAIAnswer; // 管理界面 document.getElementById('exportJsonBtn')?.addEventListener('click', ()=>{ if(!Object.keys(answerBank).length) return alert('题库为空'); const blob = new Blob([JSON.stringify(answerBank,null,2)], {type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='answerBank.json'; a.click(); }); document.getElementById('exportTxtBtn')?.addEventListener('click', ()=>{ if(!Object.keys(answerBank).length) return alert('题库为空'); const txt = Object.entries(answerBank).map(([k,v])=>`${k}|${v}`).join('\n'); const blob = new Blob([txt], {type:'text/plain'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='answerBank.txt'; a.click(); }); document.getElementById('importFileBtn')?.addEventListener('click', ()=> document.getElementById('importFileInput').click()); document.getElementById('importFileInput')?.addEventListener('change', function(e){ const file = e.target.files[0]; if(!file) return; const reader = new FileReader(); reader.onload = ev => { const data = parseAnyFormat(ev.target.result); if(data && Object.keys(data).length){ answerBank = { ...answerBank, ...data }; saveBank(); forceShowWidget(); refreshWidget(); alert(`导入成功,共 ${Object.keys(answerBank).length} 题`); } else alert('无法识别文件内容'); }; reader.readAsText(file); this.value = ''; }); document.getElementById('importTextBtn')?.addEventListener('click', ()=>{ const text = prompt('粘贴题库内容:'); if(!text) return; const data = parseAnyFormat(text); if(data && Object.keys(data).length){ answerBank = { ...answerBank, ...data }; saveBank(); forceShowWidget(); refreshWidget(); alert(`导入成功,共 ${Object.keys(answerBank).length} 题`); } else alert('无法识别内容'); }); document.getElementById('clearAllBtn')?.addEventListener('click', ()=>{ if(confirm('清空所有题目?')){ answerBank={}; saveBank(); refreshWidget(); } }); document.getElementById('aiSettingsBtn')?.addEventListener('click', showAISettings); document.querySelectorAll('.editBtn').forEach(b=>b.onclick=()=>{ const [l,f] = b.dataset.val.includes('|')?b.dataset.val.split('|'):[b.dataset.val,'']; const nk = prompt('关键字', b.dataset.key); if(nk===null) return; const nl = prompt('正确选项', l); if(nl===null) return; let nf = prompt('完整答案', f); if(nf===null) nf=''; if(nk.trim()&&nl.trim()){ if(nk!==b.dataset.key) delete answerBank[b.dataset.key]; answerBank[nk.trim()] = nf.trim()?`${nl.trim().toUpperCase()}|${nf.trim()}`:nl.trim().toUpperCase(); saveBank(); refreshWidget(); } }); document.querySelectorAll('.delBtn').forEach(b=>b.onclick=()=>{ if(confirm('删除此题?')){ delete answerBank[b.dataset.key]; saveBank(); refreshWidget(); } }); } function parseAnyFormat(text){ text = text.trim(); if(!text) return null; if(text.startsWith('{')){ try{ const obj = JSON.parse(text); if(typeof obj==='object') return obj; }catch(e){} } const lines = text.split(/\r?\n/); const lineBank = {}; let hasKV = false; for(let line of lines){ line = line.trim(); if(!line) continue; let idx = line.indexOf('|'); if(idx===-1){ idx = line.indexOf(':'); if(idx===-1) idx = line.indexOf(':'); } if(idx>0){ const k = line.substring(0,idx).trim(), v = line.substring(idx+1).trim(); if(k&&v){ lineBank[k]=v; hasKV=true; } } } if(hasKV && Object.keys(lineBank).length>0) return lineBank; const parsed = parseQuestionText(text); return parsed && Object.keys(parsed).length>0 ? parsed : null; } function parseQuestionText(text){ const blocks = text.split(/\n(?=\d+[.、.]\s)/); const bank = {}; for(let block of blocks){ block = block.trim(); if(!block) continue; const lines = block.split(/\n/); let questionLines = []; let optionLines = []; let inOption = false; for(let line of lines){ line = line.trim(); if(!line) continue; if(/^[A-D][.、.]\s/.test(line)){ inOption = true; optionLines.push(line); } else if(inOption){ if(optionLines.length>0) optionLines[optionLines.length-1] += ' ' + line; } else { questionLines.push(line); } } if(questionLines.length===0 || optionLines.length===0) continue; let fullQuestion = questionLines.join(' ').trim(); fullQuestion = fullQuestion.replace(/^\d+[.、.]\s*/, '').trim(); if(!fullQuestion) continue; let letters = ''; let texts = []; for(let optLine of optionLines){ const optRegex = /([A-D])[.、.]\s*(.*?)(?=\s*[A-D][.、.]\s|$)/g; let match; while((match = optRegex.exec(optLine)) !== null){ letters += match[1]; texts.push(match[2].trim()); } } if(letters && texts.length === letters.length){ bank[fullQuestion] = letters + '|' + texts.join(', '); } else if(letters){ bank[fullQuestion] = letters; } } return bank; } // 【修改】主循环:提取模式不再自动填入答案,只填入题目和关键字 function mainLoop(){ if(minimized){ setTimeout(mainLoop,2000); return; } const newQ = detectQuestionText(); if(newQ !== lastQuestionText){ lastQuestionText = newQ; lastAnswerInfo = matchAnswer(newQ); if(currentMode==='auto') refreshWidget(); } if(currentMode==='extract'){ const qI = document.getElementById('extQ'); if(qI && !qI.matches(':focus') && lastQuestionText && qI.value!==lastQuestionText){ qI.value = lastQuestionText; const kI = document.getElementById('extKey'); if(kI && !kI.matches(':focus')) kI.value = lastQuestionText.substring(0,20); } // 不再自动提取答案,等待用户手动点击 } setTimeout(mainLoop,2000); } refreshWidget(); mainLoop(); })();