// ==UserScript== // @name 可可英语自动答题 // @namespace https://bbs.tampermonkey.net.cn/ // @version 0.2.3 // @description 可可英语自动答题,带悬浮控制面板、暂停、连关 // @author 江禾 // @license MIT // @match https://bdc.kekenet.com/* // @include https://bdc.kekenet.com/* // @include http://bdc.kekenet.com/* // @run-at document-idle // @grant unsafeWindow // ==/UserScript== (function () { 'use strict'; const KEEP_MARKED_COUNT = 10; // ── 状态 ── let running = false; let paused = false; let isLastQuestion = false; let knownWords = []; let currentProgress = ''; let lastTotal = 0; let lastAnswered = 0; let examObserver = null; let wordObserver = null; let fillTimeoutId = null; // ── 配置 ── let cfgFontSize = 13; // ═══════════════════════════════════════ 悬浮面板 ═══════════════════════════════════════ (function initStyles() { if (document.getElementById('ke-auto-style')) return; const s = document.createElement('style'); s.id = 'ke-auto-style'; s.textContent = ` #ke-panel,#ke-panel *{box-sizing:border-box;margin:0;padding:0} @media(max-width:480px){#ke-panel{width:92vw!important;right:4vw!important;top:12px!important}} @media(min-width:481px) and (max-width:768px){#ke-panel{width:300px!important}} `; document.head.appendChild(s); })(); const GUI = (function build() { const el = document.createElement('div'); el.id = 'ke-panel'; el.style.fontSize = cfgFontSize + 'px'; Object.assign(el.style, { position:'fixed',top:'80px',right:'16px',zIndex:'99999', width:'272px',maxWidth:'100vw',background:'#fff', borderRadius:'14px',color:'#1a1a2e',userSelect:'none', overflow:'hidden',border:'1px solid #e2e6ec', boxShadow:'0 6px 28px rgba(0,0,0,.10),0 2px 6px rgba(0,0,0,.05)', fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif' }); // header const hd = ce('div',{display:'flex',alignItems:'center',justifyContent:'space-between', padding:'10px 14px',cursor:'move',borderBottom:'1px solid #f0f2f5',background:'#fafbfc'}); const title = ce('span',{fontWeight:'600',color:'#2c3e50'},'🎧 可可答题'); const status = ce('span',{fontSize:'11px',padding:'2px 8px',borderRadius:'10px', background:'#f0f2f5',color:'#888'},'待启动'); hd.append(title, status); el.appendChild(hd); // info area const info = ce('div',{padding:'12px 14px 6px'}); const progress = ce('div',{color:'#888',marginBottom:'6px',minHeight:'18px',lineHeight:'18px',wordBreak:'break-all'},'📋 等待开始…'); const question = ce('div',{color:'#555',marginBottom:'6px',minHeight:'18px',lineHeight:'18px',wordBreak:'break-all'}); const match = ce('div',{color:'#16a34a',marginBottom:'6px',minHeight:'16px',lineHeight:'16px'}); info.append(progress, question, match); el.appendChild(info); // buttons const btns = ce('div',{padding:'6px 14px',display:'flex',gap:'8px'}); const styleBtn = c => `flex:1;padding:8px 0;border:none;border-radius:8px;font-weight:500;cursor:pointer;transition:.15s;background:${c.bg};color:${c.cl}`; const btnStart = ce('button', styleBtn({bg:'#2563eb',cl:'#fff'}), '开始答题'); const btnPause = ce('button', styleBtn({bg:'#f59e0b',cl:'#fff'}) + ';opacity:.5', '暂停'); const btnStop = ce('button', styleBtn({bg:'#f0f2f5',cl:'#888'}), '停止'); btnPause.disabled = true; btnStop.disabled = true; btns.append(btnStart, btnPause, btnStop); el.appendChild(btns); // config const cfg = ce('div',{padding:'6px 14px 14px',borderTop:'1px solid #f0f2f5',marginTop:'4px'}); const row = ce('div',{display:'flex',alignItems:'center',justifyContent:'space-between'}); row.appendChild(ce('span',{color:'#555'},'🔤 字体大小')); const sizeBox = ce('div',{display:'flex',gap:'4px'}); [11,13,15].forEach(size => { const sel = size === 13; const b = ce('button',`padding:2px 8px;border-radius:6px;cursor:pointer;font-size:12px;transition:.15s;border:1px solid ${sel?'#2563eb':'#d1d5db'};background:${sel?'#dbeafe':'#fff'};color:${sel?'#2563eb':'#555'}`, String(size)); b.addEventListener('click', () => { cfgFontSize = size; el.style.fontSize = size + 'px'; sizeBox.querySelectorAll('button').forEach((x,i) => { const v = [11,13,15][i]; const s = v === size; x.style.background = s ? '#dbeafe' : '#fff'; x.style.color = s ? '#2563eb' : '#555'; x.style.borderColor = s ? '#2563eb' : '#d1d5db'; }); }); sizeBox.appendChild(b); }); row.appendChild(sizeBox); cfg.appendChild(row); el.appendChild(cfg); // drag makeDraggable(el, hd); // events btnStart.addEventListener('click', () => { if (!running) start(); }); btnPause.addEventListener('click', () => { if (running) togglePause(); }); btnStop.addEventListener('click', () => { if (running || paused) stop(); }); // expose refs el._statusEl = status; el._progressEl = progress; el._questionEl = question; el._matchEl = match; el._btnStart = btnStart; el._btnPause = btnPause; el._btnStop = btnStop; return el; })(); document.body.appendChild(GUI); // helpers function ce(tag, style, text) { const e = document.createElement(tag); if (typeof style === 'string') e.style.cssText = style; else if (style) Object.assign(e.style, style); if (text) e.textContent = text; return e; } function makeDraggable(el, handle) { let sx, sy, ix, iy; function down(e) { if (e.target.tagName === 'BUTTON') return; e.preventDefault(); const p = e.touches ? e.touches[0] : e; sx = p.clientX; sy = p.clientY; ix = el.offsetLeft; iy = el.offsetTop; document.addEventListener(e.touches ? 'touchmove' : 'mousemove', move); document.addEventListener(e.touches ? 'touchend' : 'mouseup', up); } function move(e) { const p = e.touches ? e.touches[0] : e; el.style.right = ''; el.style.left = (ix + p.clientX - sx) + 'px'; el.style.top = (iy + p.clientY - sy) + 'px'; } function up() { document.removeEventListener('touchmove', move); document.removeEventListener('mousemove', move); document.removeEventListener('touchend', up); document.removeEventListener('mouseup', up); } handle.addEventListener('mousedown', down); handle.addEventListener('touchstart', down, {passive:false}); } // ── GUI 快捷更新 ── function guiStatus(t,c='#888',b='#f0f2f5') { const s=GUI._statusEl; s.textContent=t; s.style.background=b; s.style.color=c; } function guiProgress(t) { GUI._progressEl.textContent = t; } function guiQuestion(t) { GUI._questionEl.textContent = t; } function guiMatch(t) { GUI._matchEl.textContent = t; } function guiSetButtons(state) { const bS = GUI._btnStart, bP = GUI._btnPause, bSt = GUI._btnStop; const act = (b, en, bg, cl) => { b.disabled=!en; b.style.opacity=en?'1':'.5'; b.style.background=bg; b.style.color=cl; }; if (state === 'idle') { act(bS,1,'#2563eb','#fff'); act(bP,0,'#f59e0b','#fff'); act(bSt,0,'#f0f2f5','#888'); } else if (state === 'running') { act(bS,0,'#2563eb','#fff'); act(bP,1,'#f59e0b','#fff'); bP.textContent='暂停'; act(bSt,1,'#ef4444','#fff'); } else { act(bS,0,'#2563eb','#fff'); act(bP,1,'#10b981','#fff'); bP.textContent='继续'; act(bSt,1,'#ef4444','#fff'); } } // ═══════════════════════════════════════ 初始化:标熟 + 缓存 ═══════════════════════════════════════ async function initMarks() { const items = document.querySelectorAll('.words-box .word-item'); const wordBtn = document.querySelector('.word-bottom-btn.word-btn-do'); if (items.length === 0) return false; guiProgress(`📋 正在初始化标熟… (${items.length} 个单词)`); for (let i = 0; i < items.length; i++) { const btn = items[i].querySelector('.word-item-right'); if (!btn) continue; const text = btn.textContent.trim(); const want = i < KEEP_MARKED_COUNT; if (want && text === '+标熟') btn.click(); else if (!want && text !== '+标熟') btn.click(); await new Promise(r => setTimeout(r, 150)); } knownWords = []; for (let i = KEEP_MARKED_COUNT; i < items.length; i++) { const left = items[i].querySelector('.word-item-left'); if (!left) continue; const b = left.querySelector('b'), span = left.querySelector('span'); knownWords.push({ word: b?b.textContent.trim():'', meaning: span?span.textContent.trim():'' }); } guiProgress(`📋 已缓存 ${knownWords.length} 个未标熟单词`); guiMatch(`单词: ${knownWords.map(k=>k.word).join(', ') || '无'}`); if (wordBtn) wordBtn.click(); return true; } // ═══════════════════════════════════════ 答题 ═══════════════════════════════════════ function dispatch() { if (!running || paused) return; (document.querySelector('.exam-keyboard')) ? fillInAnswer() : selectOptionAnswer(); } // ── 选择题 ── function selectOptionAnswer() { const topLeft = document.querySelector('.exam-top .exam-top-left'); if (!topLeft) return; const progressText = topLeft.textContent.trim(); const options = document.querySelectorAll('.exam-option-box .exam-option'); if (options.length === 0) { setTimeout(dispatch, 500); return; } parseProgress(progressText); guiProgress(progressText); guiQuestion(''); for (const opt of options) { const t = opt.textContent.trim(); const f = knownWords.find(k => t===k.word || t.includes(k.word) || k.word.includes(t)); if (f) { guiMatch(`✅ 匹配: "${f.word}" → "${t}"`); opt.click(); setTimeout(afterAnswer, 300); return; } } guiMatch('❌ 未匹配,已停止'); guiStatus('未匹配','#b91c1c','#fee2e2'); stop(); } // ── 填空题 ── function fillInAnswer() { const topLeft = document.querySelector('.exam-top .exam-top-left'); const progressText = topLeft ? topLeft.textContent.trim() : ''; const chineseEl = document.querySelector('.exam-info-top .exam-row.exam-question'); const chinese = chineseEl ? chineseEl.textContent.trim() : ''; const englishEl = document.querySelector('.exam-info-bottom .exam-row.exam-question'); if (!englishEl) return; const { template } = parseTemplate(englishEl); parseProgress(progressText); guiProgress(progressText); guiQuestion(chinese); let found = null; const nc = chinese.replace(/\s+/g,''); found = knownWords.find(k => { if(!k.meaning) return false; const nm=k.meaning.replace(/\s+/g,''); return nc.includes(nm)||nm.includes(nc); }); if (!found) found = knownWords.find(k => templateMatchWord(template, k.word)); if (!found) { guiMatch('❌ 未匹配,已停止'); guiStatus('未匹配','#b91c1c','#fee2e2'); stop(); return; } const missing = getMissing(template, found.word); guiMatch(`✅ "${found.word}" 待填: [${missing.join(', ')}]`); if (missing.length === 0) return; const keyboard = document.querySelector('.exam-keyboard'); typeLetters(keyboard, missing, 0, () => { confirmBtn(); afterAnswer(); }); } function templateMatchWord(template, word) { let wi = 0; for (const t of template) { if (wi >= word.length) return false; if (t.type === 'char' && t.value.toLowerCase() !== word[wi].toLowerCase()) return false; wi++; } return wi === word.length; } function confirmBtn() { document.querySelectorAll('.exam-key').forEach(k => { if (k.textContent.trim()==='确定') k.click(); }); } function parseTemplate(el) { const t=[], fs=[]; el.childNodes.forEach(c => { if (c.nodeType===1 && c.classList.contains('fill-space')) { t.push({type:'fill'}); fs.push(c); } else if (c.nodeType===3) { for (const ch of c.textContent) t.push({type:'char',value:ch}); } }); return {template:t, fillSpaces:fs}; } function getMissing(template, word) { const l=[]; let wi=0; for (const t of template) { if (t.type==='fill') l.push(witypeLetters(keyboard,letters,idx,onDone),300); return; } if (idx >= letters.length) { fillTimeoutId=null; setTimeout(onDone,200); return; } const target = letters[idx]; keyboard.querySelectorAll('.exam-key').forEach(k => { if(k.textContent.trim().toLowerCase()===target.toLowerCase()) k.click(); }); fillTimeoutId = setTimeout(()=>typeLetters(keyboard,letters,idx+1,onDone),200); } // ═══════════════════════════════════════ 进度 & 连关 ═══════════════════════════════════════ function parseProgress(text) { const m = text.match(/(\d+)\/(\d+)/); if (m) { lastAnswered = +m[1]; lastTotal = +m[2]; } } function afterAnswer() { if (lastTotal && lastAnswered >= lastTotal) isLastQuestion = true; } function onExamPageGone() { if (!running || paused || !isLastQuestion) return; stopExamWatcher(); guiProgress('📋 等待下一关...'); guiStatus('连关中','#7c3aed','#ede9fe'); pollNextBtn(0); } function pollNextBtn(tries) { if (!running || paused) return; if (tries > 40) { guiProgress('⚠ 连关超时,请手动操作'); return; } const btn = document.querySelector('.res-btn-next'); if (!btn) { setTimeout(()=>pollNextBtn(tries+1), 500); return; } // 等待 Vue 水合完成再点击 if (tries < 3) { setTimeout(()=>pollNextBtn(tries+1), 300); return; } btn.click(); btn.dispatchEvent(new MouseEvent('click', {bubbles:true,cancelable:true,view:unsafeWindow})); knownWords = []; lastTotal = 0; lastAnswered = 0; currentProgress = ''; isLastQuestion = false; setTimeout(() => { if (running && !paused) watchForWordsBox(); }, 1000); } // ═══════════════════════════════════════ 监听 ═══════════════════════════════════════ function startExamWatcher() { currentProgress = ''; if (examObserver) examObserver.disconnect(); examObserver = new MutationObserver(() => { if (!running || paused) return; const tl = document.querySelector('.exam-top .exam-top-left'); if (!tl) { if (isLastQuestion) onExamPageGone(); return; } const t = tl.textContent.trim(); if (t === currentProgress) return; currentProgress = t; setTimeout(dispatch, 300); }); examObserver.observe(document.documentElement, {childList:true,subtree:true,characterData:true}); if (document.querySelector('.exam-top .exam-top-left')) { currentProgress=''; dispatch(); } } function stopExamWatcher() { if (examObserver) { examObserver.disconnect(); examObserver=null; } } function stopWordWatcher() { if (wordObserver) { wordObserver.disconnect(); wordObserver=null; } } function watchForWordsBox() { stopWordWatcher(); wordObserver = new MutationObserver(() => { const box = document.querySelector('.words-box'); if (box && box.children.length > 0) { stopWordWatcher(); bootExam(); } }); wordObserver.observe(document.documentElement, {childList:true,subtree:true}); if (document.querySelector('.words-box')?.children.length > 0) { stopWordWatcher(); bootExam(); } } async function bootExam() { const ok = await initMarks(); if (ok && running && !paused) { startExamWatcher(); guiProgress('📋 题目监听中…'); guiStatus('运行中','#2563eb','#dbeafe'); } } // ═══════════════════════════════════════ 控制 ═══════════════════════════════════════ function start() { running=true; paused=false; guiSetButtons('running'); guiStatus('运行中','#2563eb','#dbeafe'); watchForWordsBox(); } function togglePause() { paused = !paused; if (paused) { guiSetButtons('paused'); guiStatus('已暂停','#d97706','#fef3c7'); guiProgress('⏸ 已暂停 — 点击「继续」恢复'); } else { guiSetButtons('running'); guiStatus('运行中','#2563eb','#dbeafe'); guiProgress('📋 题目监听中…'); if (fillTimeoutId) { clearTimeout(fillTimeoutId); fillTimeoutId=null; } dispatch(); } } function stop() { running=false; paused=false; stopExamWatcher(); stopWordWatcher(); if (fillTimeoutId) { clearTimeout(fillTimeoutId); fillTimeoutId=null; } knownWords=[]; lastTotal=0; lastAnswered=0; isLastQuestion=false; guiSetButtons('idle'); guiStatus('已停止','#888','#f0f2f5'); guiProgress('⏸ 已停止'); guiMatch(''); } guiStatus('待启动'); guiProgress('📋 点击「开始答题」启动'); })();