// ==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 ``;
}
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 `| ${i+1} | ${k.substring(0,20)} | ${l} | |
`; }).join('')}
` : '📭 题库为空
';
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 = `
`;
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();
})();