// ==UserScript== // @name 柳大师 - 学习助手(v10.9.1) // @namespace local.liudashi.study-helper // @version 10.9.2 // @description 视频+课件任务队列修复版 // @author 柳大师 // @match *://*.chaoxing.com/* // @match *://*.fanya.chaoxing.com/* // @match *://i.chaoxing.com/* // @match *://passport2.chaoxing.com/* // @match *://*.zhihuishu.com/* // @match *://*.icourse163.org/* // @match *://*.xuetangx.com/* // @match *://*.icve.com.cn/* // @match *://*.ulearning.cn/* // @match *://*.cnmooc.org/* // @match *://*.edu.cn/* // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @connect open.bigmodel.cn // @connect api.deepseek.com // @connect dashscope.aliyuncs.com // @connect api.gomooc.net // @connect * // @run-at document-idle // ==/UserScript== (function () { 'use strict'; if (window._sh_script_injected) return; window._sh_script_injected = true; const BRAND = '柳大师'; const VERSION = '10.9.1'; const IS_TOP = window.top === window.self; function getGreeting() { const h = new Date().getHours(); if (h < 6) return '凌晨好'; if (h < 11) return '上午好'; if (h < 13) return '中午好'; if (h < 18) return '下午好'; return '晚上好'; } const sleep = ms => new Promise(r => setTimeout(r, ms)); const rand = (a, b) => a + Math.random() * (b - a); const log = (...a) => console.log(`[${BRAND}]`, ...a); const LOCK_KEY = 'sh_video_lock_v8'; const FRAME_ID = 'f_' + Math.random().toString(36).slice(2, 10) + '_' + Date.now(); let HOST = null, ROOT = null, BALL_EL = null, PANEL_EL = null; function readLock() { try { const r = localStorage.getItem(LOCK_KEY); if (!r) return null; const d = JSON.parse(r); return (Date.now() - d.heartbeat > 30000) ? null : d; } catch (e) { return null; } } function writeLock(d) { try { localStorage.setItem(LOCK_KEY, JSON.stringify(d)); } catch (e) {} } function clearLock() { try { if (readLock()?.frameId === FRAME_ID) localStorage.removeItem(LOCK_KEY); } catch (e) {} } function acquireLock(src) { const now = Date.now(); let d = readLock(); if (!d) { writeLock({ frameId: FRAME_ID, src: src || '', heartbeat: now }); return true; } if (d.frameId === FRAME_ID) { d.heartbeat = now; writeLock(d); return true; } return false; } function lockHeldByOther() { const d = readLock(); return d && d.frameId !== FRAME_ID; } setInterval(() => { const d = readLock(); if (d?.frameId === FRAME_ID) { d.heartbeat = Date.now(); writeLock(d); } }, 10000); const AI_PROVIDERS = { zhipu: { name: '智谱 GLM-4-Flash', url: 'https://open.bigmodel.cn/api/paas/v4/chat/completions', model: 'glm-4-flash', tip: '完全免费 · 手机号注册', registerUrl: 'https://open.bigmodel.cn/' }, deepseek: { name: 'DeepSeek', url: 'https://api.deepseek.com/v1/chat/completions', model: 'deepseek-chat', tip: '注册送额度', registerUrl: 'https://platform.deepseek.com/' }, qwen: { name: '通义千问', url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', model: 'qwen-turbo', tip: '新用户免费额度', registerUrl: 'https://dashscope.console.aliyun.com/' }, }; const CONFIG = { enabled: GM_getValue('sh_enabled', true), autoPlay: GM_getValue('sh_autoPlay', true), autoNext: GM_getValue('sh_autoNext', true), autoAnswer: GM_getValue('sh_autoAnswer', true), aiEnabled: GM_getValue('sh_aiEnabled', true), autoSubmit: GM_getValue('sh_autoSubmit', false), stealthMode: GM_getValue('sh_stealthMode', true), muteFallback: GM_getValue('sh_muteFallback', true), playbackRate: GM_getValue('sh_playbackRate', 1.5), nextDelay: GM_getValue('sh_nextDelay', 10), darkMode: GM_getValue('sh_darkMode', false), aiProvider: GM_getValue('sh_aiProvider', 'zhipu'), aiKeys: GM_getValue('sh_aiKeys', {}), enableFreeBank: GM_getValue('sh_enableFreeBank', true), ballPos: GM_getValue('sh_ballPos', { side: 'right', top: 100 }), }; function saveConfig() { try { Object.keys(CONFIG).forEach(k => GM_setValue('sh_' + k, CONFIG[k])); } catch (e) {} } function domGet(id) { try { return ROOT?.getElementById(id); } catch (e) { return null; } } function domQueryAll(sel) { try { return ROOT ? Array.from(ROOT.querySelectorAll(sel)) : []; } catch (e) { return []; } } function loadBank() { try { return GM_getValue('sh_qbank', {}); } catch (e) { return {}; } } function saveBank(b) { try { GM_setValue('sh_qbank', b); } catch (e) {} } function hashQ(t) { const c = (t || '').replace(/\s+/g, '').slice(0, 200); let h = 0; for (let i = 0; i < c.length; i++) { h = ((h << 5) - h) + c.charCodeAt(i); h |= 0; } return 'q_' + Math.abs(h).toString(36); } function queryBank(t) { const b = loadBank(); const e = b[hashQ(t)]; return e ? e.a : null; } function recordBank(t, a, type, src) { if (!t || !a) return false; const b = loadBank(); const k = hashQ(t); const ex = b[k]; if (ex && ex.src === 'system' && ex.a === a) return false; if (ex && ex.src === 'system' && src === 'ai') return false; b[k] = { q: t.slice(0, 300), a, type: type || 'radio', t: Date.now(), src: src || 'ai' }; const keys = Object.keys(b); if (keys.length > 5000) { keys.sort((x, y) => (b[x].t || 0) - (b[y].t || 0)); keys.slice(0, 1000).forEach(kk => delete b[kk]); } saveBank(b); return true; } function deleteBankEntry(k) { const b = loadBank(); if (b[k]) { delete b[k]; saveBank(b); return true; } return false; } function clearBank() { saveBank({}); } function listBank(f) { const b = loadBank(); let arr = Object.entries(b).map(([k, v]) => ({ key: k, ...v })); if (f) { const s = f.toLowerCase(); arr = arr.filter(it => (it.q || '').toLowerCase().includes(s) || (it.a || '').toLowerCase().includes(s)); } return arr.sort((a, b) => (b.t || 0) - (a.t || 0)); } function bankStats() { const b = loadBank(); let sys = 0, ai = 0, man = 0; Object.values(b).forEach(v => { if (v.src === 'system') sys++; else if (v.src === 'manual') man++; else ai++; }); return { total: Object.keys(b).length, sys, ai, man }; } const TASK_CARD_SELECTORS = [ '.ans-job-item', '.posCatalog_select', '.chapter_item', '.catalog_points', '.jobItem', '[class*="ans-job"]', '[class*="job-item"]', '[class*="catalog_points"]', ]; const COMPLETED_MARKERS = [ '.icon_Completed', '.ans-job-icon-come', '.ans-job-icon.ans-job-icon-come', '.icon-dui', '.icon-success', '[class*="complete"]', '[class*="finish"]', '[class*="done"]', '[class*="success"]', '[class*="dui"]', '[class*="passed"]', '[class*="checked"]', ]; const PPT_FRAME_HINTS = /ppt|slide|panview|document|pdf|doc|viewer|office/i; const PPT_NEXT_BTN_SELECTORS = [ '.ppt_next', '.nextBtn', '.next-btn', '.btn-next', '.slide-next', '[title*="下一页"]', '[aria-label*="下一页"]', '[class*="next-page"]', '[class*="nextPage"]', '[class*="next_page"]', '.arrow-right', '.arrowRight', '.icon-next', '[class*="arrow"][class*="right"]', 'a[data-page="next"]', ]; function isGreenColor(str) { if (!str) return false; if (str.includes('green') || str.includes('success') || str.includes('complete') || str.includes('done')) return true; const m = str.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); if (m) { const [r, g, b] = m.slice(1, 4).map(Number); return g > 120 && g > r * 1.15 && g > b * 1.15; } return false; } function isCardCompleted(card) { if (!card) return false; for (const sel of COMPLETED_MARKERS) { const marks = card.querySelectorAll(sel); for (const mark of marks) { const st = getComputedStyle(mark); if (isGreenColor(st.color) || isGreenColor(st.backgroundColor) || isGreenColor(st.borderColor)) return true; try { const bf = getComputedStyle(mark, '::before'); if (/["']?[✓✔√]/.test(bf.content || '')) return true; if (isGreenColor(bf.color) || isGreenColor(bf.backgroundColor)) return true; } catch (e) {} const lb = (mark.getAttribute('aria-label') || mark.getAttribute('title') || '').trim(); if (/完成|已学|已看/.test(lb)) return true; } } const textEls = card.querySelectorAll('span, div, em, i, p, b, strong, label'); for (const el of textEls) { const t = (el.textContent || '').trim(); if (t.length > 15) continue; if (/^(任务点已完成|已完成|已学完|完成学习|学习完成|通过|已通过)$/.test(t)) { const st = getComputedStyle(el); if (isGreenColor(st.color)) return true; const p = el.parentElement; if (p) { const ps = getComputedStyle(p); if (isGreenColor(ps.color) || isGreenColor(ps.backgroundColor)) return true; } } } return false; } function findVideoInCard(card) { if (!card) return null; let v = card.querySelector('video'); if (v) return v; const ifr = card.querySelectorAll('iframe'); for (const f of ifr) { try { const d = f.contentDocument || f.contentWindow?.document; if (d) { v = d.querySelector('video'); if (v) return v; } } catch (e) {} } return null; } function getCardType(card) { if (!card) return 'unknown'; const v = findVideoInCard(card); if (v) return 'video'; const text = card.textContent || ''; if (/测验|答题|选择题|判断题|考试|考核/.test(text)) return 'quiz'; if (/视频|视屏|课程视频|观看/.test(text)) return 'video'; if (/课件|文档|阅读|ppt|PPT|幻灯片/.test(text)) return 'ppt'; if (/pdf|PDF/.test(text)) return 'doc'; const cls = (card.className || '').toString(); if (/video/i.test(cls)) return 'video'; if (/ppt|slide|courseware/i.test(cls)) return 'ppt'; if (/doc|read|pdf/i.test(cls)) return 'doc'; if (/quiz|exam|test/i.test(cls)) return 'quiz'; const ifr = card.querySelectorAll('iframe'); for (const f of ifr) { if (PPT_FRAME_HINTS.test(f.src || '')) return 'ppt'; } return 'unknown'; } function scanTaskCards() { const cardSet = new Set(); for (const sel of TASK_CARD_SELECTORS) { try { document.querySelectorAll(sel).forEach(el => cardSet.add(el)); } catch (e) {} } const cards = [...cardSet].sort((a, b) => { const p = a.compareDocumentPosition(b); if (p & Node.DOCUMENT_POSITION_FOLLOWING) return -1; if (p & Node.DOCUMENT_POSITION_PRECEDING) return 1; return 0; }); const queue = []; for (const card of cards) { const rect = card.getBoundingClientRect(); if (rect.width < 50 || rect.height < 20) continue; if (isCardCompleted(card)) continue; const type = getCardType(card); if (type === 'unknown') continue; queue.push({ card, type, video: findVideoInCard(card), text: (card.textContent || '').trim().slice(0, 60) }); } return queue; } let CURRENT_TASK = null, IS_SWITCHING = false, VIDEO_ENDED_AT = 0, LAST_ACTION = 0, LAST_SCAN_AT = 0; const SCAN_INTERVAL = 1500; const TASK_TIMEOUT = 3 * 60 * 1000; const VIDEO_STALL_TIMEOUT = 60 * 1000; const PPT_STALL_TIMEOUT = 60 * 1000; async function playVideoElement(v) { if (!v) return; applyRateDynamic(v, CONFIG.playbackRate); try { await v.play(); updateStatus('播放中'); } catch (e) { if (CONFIG.muteFallback && !v.muted) { v.muted = true; try { await v.play(); updateStatus('播放中(静音)'); } catch (e2) {} } } } function findPptFrame(card) { if (!card) return null; const iframes = [...card.querySelectorAll('iframe')]; for (const f of iframes) { if (PPT_FRAME_HINTS.test(f.src || '')) return f; } for (const f of iframes) { try { const d = f.contentDocument; if (d && d.body) return f; } catch (e) {} } return null; } function tryClickPptNext(pageDoc) { if (!pageDoc) return false; for (const sel of PPT_NEXT_BTN_SELECTORS) { try { const btns = pageDoc.querySelectorAll(sel); for (const b of btns) { const style = pageDoc.defaultView ? pageDoc.defaultView.getComputedStyle(b) : null; if (style && (style.display === 'none' || style.visibility === 'hidden')) continue; if (b.disabled) continue; const txt = (b.textContent || '').trim(); const title = (b.getAttribute('title') || b.getAttribute('aria-label') || '').trim(); if (/上一页|prev|previous/i.test(txt + ' ' + title)) continue; try { b.click(); return true; } catch (e) {} try { b.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); return true; } catch (e) {} } } catch (e) {} } try { const allClickable = pageDoc.querySelectorAll('a, button, span, div[onclick]'); for (const el of allClickable) { const t = (el.textContent || '').trim(); if (t.length > 10) continue; if (/^(下一页|下一张|继续|next)$/i.test(t)) { try { el.click(); return true; } catch (e) {} } } } catch (e) {} return false; } function tryPressPptKey(pageDoc) { if (!pageDoc) return; const targets = [pageDoc.body, pageDoc.documentElement].filter(Boolean); ['ArrowRight', 'PageDown'].forEach(k => { targets.forEach(t => { try { t.dispatchEvent(new KeyboardEvent('keydown', { key: k, bubbles: true })); t.dispatchEvent(new KeyboardEvent('keyup', { key: k, bubbles: true })); } catch (e) {} }); }); } async function handlePptTask(task) { if (isCardCompleted(task.card)) return 'done'; const frame = findPptFrame(task.card); let pageDoc = null; try { pageDoc = frame ? (frame.contentDocument || frame.contentWindow?.document) : null; } catch (e) { pageDoc = null; } if (!pageDoc) pageDoc = task.card.ownerDocument; if (tryClickPptNext(pageDoc)) return 'clicked'; if (pageDoc !== task.card.ownerDocument) { if (tryClickPptNext(task.card.ownerDocument)) return 'clicked'; } tryPressPptKey(pageDoc); return 'key'; } async function pollLoop() { if (!CONFIG.enabled || !CONFIG.autoPlay || IS_SWITCHING) return; if (lockHeldByOther()) { document.querySelectorAll('video').forEach(v => { if (!v.paused) try { v.pause(); } catch (e) {} }); return; } const now = Date.now(); if (CURRENT_TASK && CURRENT_TASK.startTime && (now - CURRENT_TASK.startTime > TASK_TIMEOUT)) { log('⏱ 任务超时 3 分钟,强制释放:', CURRENT_TASK.type); releaseTask(); } if (CURRENT_TASK && CURRENT_TASK.card && isCardCompleted(CURRENT_TASK.card)) { log('✅ 卡片变绿,任务完成'); await finishTask(CURRENT_TASK, true); return; } if (CURRENT_TASK) { const task = CURRENT_TASK; if (!task.card || !document.contains(task.card)) { log('📤 卡片已移除,释放'); releaseTask(); return; } if (task.type === 'quiz') { const hasQ = task.card.querySelector('.TiMu, [class*="question"]'); if (!hasQ) { log('⚠️ quiz 卡片没有题目元素,跳过'); releaseTask(); return; } updateStatus('答题中...'); return; } if (task.type === 'ppt' || task.type === 'doc') { if (!task.entered) { task.entered = true; task.startTime = now; task.lastActionTime = now; log('📄 进入课件任务'); try { task.card.click(); } catch (e) {} await sleep(2000); } if (task.lastActionTime && (now - task.lastActionTime > PPT_STALL_TIMEOUT)) { log('⚠️ 课件 60 秒无进展,释放'); releaseTask(); return; } if (!task.lastActionTime || now - task.lastActionTime > 3000) { task.lastActionTime = now; const result = await handlePptTask(task); if (result === 'done') { await finishTask(task, true); return; } log('📄 课件翻页:', result); } updateStatus('课件翻页中'); return; } if (task.type === 'video') { const v = task.video; if (!v || !document.contains(v)) { log('⚠️ 视频元素丢失,尝试重新查找'); const rv = findVideoInCard(task.card); if (rv) { task.video = rv; return; } log('❌ 视频真的找不到了,释放'); releaseTask(); return; } if (!task.entered) { task.entered = true; task.startTime = now; task.lastActionTime = now; log('▶ 进入视频任务'); try { task.card.click(); } catch (e) {} await sleep(1500); await playVideoElement(v); return; } const ended = v.ended || (v.duration && v.currentTime / v.duration >= 0.99); if (ended && !VIDEO_ENDED_AT) { VIDEO_ENDED_AT = Date.now(); updateStatus('等待任务点变绿'); log('视频播放完毕,等待变绿'); } if (ended && VIDEO_ENDED_AT && Date.now() - VIDEO_ENDED_AT > 90000) { log('⚠️ 视频结束 90 秒未变绿,强制通过'); await finishTask(task, true); return; } if (v.paused && !ended) { if (task.lastActionTime && (now - task.lastActionTime > VIDEO_STALL_TIMEOUT)) { log('⚠️ 视频 60 秒无进展,释放'); releaseTask(); return; } try { await v.play(); applyRateDynamic(v, CONFIG.playbackRate); task.lastActionTime = now; updateStatus('播放中'); } catch (e) { v.muted = true; try { await v.play(); updateStatus('播放中(静音)'); task.lastActionTime = now; } catch (e2) {} } } else if (!ended) { applyRateDynamic(v, CONFIG.playbackRate); updateStatus('播放中'); task.lastActionTime = now; } return; } log('❓ 未知任务类型,释放:', task.type); releaseTask(); return; } if (now - LAST_SCAN_AT < SCAN_INTERVAL) return; LAST_SCAN_AT = now; const queue = scanTaskCards(); if (!queue.length) { const quizCount = document.querySelectorAll('.TiMu').length; if (quizCount > 0) return; const playingVideos = [...document.querySelectorAll('video')].filter(v => !v.paused && !v.ended); if (playingVideos.length > 0) return; if (IS_TOP) await goNext(); return; } const head = queue[0]; log('▶ 取出任务 [', head.type, ']:', head.text); if (head.type === 'video') { const v = findVideoInCard(head.card) || head.video; if (!v) { log('⚠️ video 卡片但找不到 video,跳过'); head.card.dataset.shSkipped = '1'; return; } if (!acquireLock(v.currentSrc || 'video')) return; CURRENT_TASK = { card: head.card, type: 'video', video: v, startTime: Date.now(), lastActionTime: Date.now(), entered: false, }; return; } if (head.type === 'ppt' || head.type === 'doc') { CURRENT_TASK = { card: head.card, type: head.type, video: null, startTime: Date.now(), lastActionTime: Date.now(), entered: false, }; return; } if (head.type === 'quiz') { CURRENT_TASK = { card: head.card, type: 'quiz', video: null, startTime: Date.now(), entered: false, }; try { head.card.click(); } catch (e) {} return; } } async function finishTask(task, force = false) { if (!task) return; if (task.card) try { task.card.dataset.shTaskDone = '1'; } catch (e) {} if (task.video) try { task.video.dataset.shTaskDone = '1'; task.video.pause(); } catch (e) {} const delay = 3000 + Math.random() * 12000; updateStatus(`延时 ${(delay / 1000).toFixed(0)}s`); clearLock(); releaseTask(); await sleep(delay); setTimeout(() => pollLoop().catch(() => {}), 100); } function releaseTask() { if (CURRENT_TASK?.video) try { CURRENT_TASK.video.dataset.shActive = '0'; } catch (e) {} CURRENT_TASK = null; VIDEO_ENDED_AT = 0; } let lastNext = 0; async function goNext() { const now = Date.now(); if (now - lastNext < 30000) return; lastNext = now; IS_SWITCHING = true; updateStatus('切换中...'); const cooldown = CONFIG.stealthMode ? rand(180000, 480000) : 0; await sleep(CONFIG.nextDelay * 1000 + cooldown); tryClickNext(); setTimeout(() => { IS_SWITCHING = false; setTimeout(() => pollLoop().catch(() => {}), 3000); }, 8000); } function tryClickNext() { const sels = ['.next', '.next-btn', '.nextSection', '.next-section', '.chapter-next', '.section-next', '#nextBtn', '[class*="nextBtn"]', '[class*="next"] button', '.nextChapter', '.next-chapter', '[title*="下一"]', 'a[href*="next"]']; for (const s of sels) { const b = document.querySelector(s); if (b && b.offsetParent !== null && !b.disabled) { b.click(); return true; } } for (const el of document.querySelectorAll('a,button,span,div[onclick]')) { const t = (el.textContent || '').trim(); if (t && t.length <= 20 && /^下一[节章课]|继续学习|next$/i.test(t) && el.offsetParent !== null) { el.click(); return true; } } return false; } async function queryFreeBank(q) { if (!CONFIG.enableFreeBank) return null; return new Promise(res => { GM_xmlhttpRequest({ method: 'GET', url: `https://api.gomooc.net/api.php?question=${encodeURIComponent(q)}`, timeout: 5000, onload: r => { try { const d = JSON.parse(r.responseText); const a = d.answer || (d.data && d.data.answer); res(a || null); } catch (e) { res(null); } }, onerror: () => res(null), ontimeout: () => res(null) }); }); } async function callAI(q, opts, type) { if (!CONFIG.aiEnabled) return null; const prov = AI_PROVIDERS[CONFIG.aiProvider]; const key = (CONFIG.aiKeys || {})[CONFIG.aiProvider]; if (!prov || !key) { log(`❌ 未配置 ${prov?.name || 'AI'} 的 Key`); return null; } let sys, usr; if (type === 'radio' || type === 'checkbox') { const optTexts = opts.map(o => o.text || o); const isJudge = opts.length === 2 && optTexts.some(t => /对|错|是|否|正确|错误/.test(t)); if (isJudge) { sys = `你是一位答题助手。只输出"对"或"错",不要任何解释。如果题目有乱码,根据上下文推测原题再判断。`; usr = `判断题:${q}\n\n只回答"对"或"错":`; } else { const optText = opts.map((o, i) => `${String.fromCharCode(65 + i)}. ${o.text || o}`).join('\n'); sys = `你是一位答题助手。只输出选项字母(如A或AC),不要任何解释。如果题目有乱码,根据上下文推测原题再作答。`; usr = `题目:${q}\n\n选项:\n${optText}\n\n只回答选项字母:`; } } else if (type === 'blank') { sys = `你是一位答题助手。只输出答案内容,多空用||分隔,不要解释。`; usr = `填空题:${q}\n\n只回答答案内容:`; } else { sys = `你是一位答题助手。只输出答案内容,不要解释。`; usr = `题目:${q}\n答案:`; } log(`🤖 调用 ${prov.name}`); const res = await new Promise(r => { GM_xmlhttpRequest({ method: 'POST', url: prov.url, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${key}` }, data: JSON.stringify({ model: prov.model, messages: [{ role: 'system', content: sys }, { role: 'user', content: usr }], max_tokens: 200, temperature: 0.1 }), timeout: 20000, onload: x => { log('📥 AI 状态:', x.status); try { const d = JSON.parse(x.responseText); if (d.error) { log('❌ AI 错误:', JSON.stringify(d.error).slice(0, 200)); r(null); return; } const t = d.choices[0].message.content.trim(); log('✅ AI 返回:', t); r(t); } catch (e) { log('❌ 解析失败:', e.message); r(null); } }, onerror: () => { log('❌ AI 网络错误'); r(null); }, ontimeout: () => { log('❌ AI 超时'); r(null); } }); }); if (!res) return null; return extractAnswerFromText(res, type, opts); } function extractAnswerFromText(text, type, opts) { if (!text) return null; const t = String(text).trim(); if (type === 'radio' || type === 'checkbox') { const optTexts = (opts || []).map(o => o.text || o); const isJudge = (opts || []).length === 2 && optTexts.some(o => /对|错|是|否|正确|错误/.test(o)); if (isJudge) { if (/(?:答案|正确答案)[::是\s]*错/.test(t) || /^错/.test(t)) return '错'; if (/(?:答案|正确答案)[::是\s]*对/.test(t) || /^对/.test(t)) return '对'; if (/[×✘]/.test(t)) return '错'; if (/[√✔]/.test(t)) return '对'; if (/错误/.test(t) && !/正确/.test(t)) return '错'; if (/正确/.test(t) && !/错误/.test(t)) return '对'; return t.slice(0, 3); } const m1 = t.match(/(?:答案|正确答案|选|应选|答案为|答案是)[::是\s]*([A-D]{1,4})/i); if (m1) return m1[1].toUpperCase(); const m2 = t.match(/^[A-D]{1,4}$/); if (m2) return t.toUpperCase(); const letters = t.match(/[A-D]/g); if (letters && letters.length > 0 && letters.length <= 4) return [...new Set(letters)].sort().join(''); return t.slice(0, 20); } if (type === 'blank' || type === 'short') { let a = t.replace(/^(?:答案|正确答案|答案为|答案是)[::是\s]*/, '').trim(); a = a.replace(/^["'「『]/, '').replace(/["'」』]$/, ''); return a || null; } return t; } function inViewport(el) { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0 && r.bottom > 0 && r.right > 0 && r.top < innerHeight && r.left < innerWidth; } function isQuestionText(t) { if (!t || t.length < 3) return false; const cn = t.match(/[\u4e00-\u9fa5]/g); if (cn && cn.length / t.length >= 0.1) return true; return /^[a-zA-Z0-9\s.,;:'"!?()\-]+$/.test(t) && t.length > 8; } const Q_SELS = ['.TiMu', '.question-item', '.topic-item', '[class*="question"]', '[class*="exam-item"]', '.pop-question', '.quiz-item', '.ans-video-question', '[class*="anser"]', '[class*="qt-content"]', '.questionLi', '[class*="Zy_TItle"]']; function extractQuestion(item) { if (!item || !inViewport(item)) return null; const tEl = item.querySelector('.Zy_TItle,.question-title,.title,.stem,[class*="title"],[class*="stem"],[class*="topic"]'); if (!tEl) return null; let qt = tEl.textContent.trim().replace(/^\d+[\.、]\s*/, '').replace(/【.*?】/g, '').trim(); if (!isQuestionText(qt)) return null; const radios = item.querySelectorAll('input[type="radio"]'); const checks = item.querySelectorAll('input[type="checkbox"]'); const textInputs = item.querySelectorAll('input[type="text"], textarea, .blank-input, .fill-input'); let type = 'radio', inputEls = []; if (checks.length > 0 && radios.length === 0) { type = 'checkbox'; inputEls = [...checks]; } else if (radios.length > 0) { type = 'radio'; inputEls = [...radios]; } else if (textInputs.length > 0) { type = 'blank'; inputEls = [...textInputs]; } else type = 'short'; const options = []; if (type === 'radio' || type === 'checkbox') { inputEls.forEach((inp, idx) => { const labelEl = inp.closest('label') || inp.parentElement; let optText = '', letter = ''; const letterEl = labelEl?.querySelector('.option-letter,.letter,.num,[class*="letter"]'); if (letterEl) { letter = letterEl.textContent.trim().replace(/[^A-Da-d]/g, '').toUpperCase(); const clone = labelEl.cloneNode(true); const lc = clone.querySelector('.option-letter,.letter,.num,[class*="letter"]'); if (lc) lc.remove(); optText = clone.textContent.trim(); } else if (labelEl) { const raw = labelEl.textContent.trim(); const m = raw.match(/^([A-Da-d])[\.、\s、]*(.+)$/); if (m) { letter = m[1].toUpperCase(); optText = m[2].trim(); } else optText = raw; } optText = optText.replace(/\s+/g, ' ').trim(); if (!letter) letter = String.fromCharCode(65 + idx); options.push({ letter, text: optText, inputEl: inp, labelEl }); }); } else if (type === 'blank') { inputEls.forEach((inp, idx) => { options.push({ letter: String(idx + 1), text: '', inputEl: inp, labelEl: inp.parentElement }); }); } return { element: item, text: qt, type, options, inputEls }; } function parseAnswer(answerText, question) { if (!answerText) return []; const raw = String(answerText).trim(); if (question.type === 'radio' && question.options.length === 2) { const optTexts = question.options.map(o => o.text); const isJudge = optTexts.some(t => /对|错|是|否|正确|错误|T|F|√|×/.test(t)); if (isJudge) { const firstIsTrue = /对|正确|是|√|T/.test(optTexts[0]) && !/错|错误|否|×|F/.test(optTexts[0]); const ansUpper = raw.toUpperCase(); const ansIsTrue = /[√✔对是正确T]/.test(ansUpper) && !/[×✘错否错误F]/.test(ansUpper); const ansIsFalse = /[×✘错否错误F]/.test(ansUpper) && !/[√✔对是正确T]/.test(ansUpper); if (ansIsTrue) return [firstIsTrue ? 0 : 1]; if (ansIsFalse) return [firstIsTrue ? 1 : 0]; for (let i = 0; i < optTexts.length; i++) { if (optTexts[i].includes(raw) || raw.includes(optTexts[i])) return [i]; } } } const letters = raw.toUpperCase().match(/[A-D]/g); if (letters && letters.length > 0) { const idxs = [...new Set(letters)].map(l => l.charCodeAt(0) - 65).filter(i => i >= 0 && i < question.options.length); if (idxs.length > 0) return idxs; } const clean = s => String(s || '').replace(/[\s,。、,.\-_()()【】\[\];;::""''!!??]/g, ''); const ansClean = clean(raw); if (!ansClean) return []; const matched = []; question.options.forEach((opt, idx) => { const oc = clean(opt.text); if (!oc) return; if (oc === ansClean) { matched.push(idx); return; } if (oc.length >= 2 && (oc.includes(ansClean) || ansClean.includes(o