// ==UserScript== // @name 柳大师 - 学习助手(共享题库版) // @namespace local.liudashi.study-helper // @version 9.2.0 // @description 任务队列 + 共享题库 + 免费AI + 低风控 // @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 generativelanguage.googleapis.com // @connect api.deepseek.com // @connect dashscope.aliyuncs.com // @connect api.gomooc.net // @connect raw.githubusercontent.com // @connect gist.githubusercontent.com // @connect * // @run-at document-idle // ==/UserScript== (function () { 'use strict'; if (window._sh_script_injected) return; window._sh_script_injected = true; const BRAND = '柳大师'; const VERSION = '9.2.0'; const IS_TOP = window.top === window.self; // ==================== 跨 frame 全局锁 ==================== const LOCK_KEY = 'sh_video_lock_v4'; 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 raw = localStorage.getItem(LOCK_KEY); if (!raw) return null; const d = JSON.parse(raw); return (Date.now() - d.heartbeat > 30000) ? null : d; } catch (e) { return null; } } function writeLock(data) { try { localStorage.setItem(LOCK_KEY, JSON.stringify(data)); } 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); // ==================== AI 服务商 ==================== const AI_PROVIDERS = { zhipu: { name: '智谱 GLM-4-Flash', url: 'https://open.bigmodel.cn/api/paas/v4/chat/completions', model: 'glm-4-flash', apiStyle: 'openai', tip: '完全免费 · 手机号注册', registerUrl: 'https://open.bigmodel.cn/' }, deepseek: { name: 'DeepSeek', url: 'https://api.deepseek.com/v1/chat/completions', model: 'deepseek-chat', apiStyle: 'openai', tip: '注册送额度', registerUrl: 'https://platform.deepseek.com/' }, qwen: { name: '通义千问', url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', model: 'qwen-turbo', apiStyle: 'openai', tip: '新用户免费额度', registerUrl: 'https://dashscope.console.aliyun.com/' }, gemini: { name: 'Gemini 1.5', url: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent', model: 'gemini-1.5-flash', apiStyle: 'gemini', tip: '需要网络环境', registerUrl: 'https://aistudio.google.com/apikey' }, }; // ==================== 配置 ==================== 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), 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), sharedBankUrl: GM_getValue('sh_sharedBankUrl', 'https://raw.githubusercontent.com/liudashi-study/shared-bank/main/bank.json'), enableSharedBank: GM_getValue('sh_enableSharedBank', true), panelExpanded: GM_getValue('sh_panelExpanded', false), ballPos: GM_getValue('sh_ballPos', { side: 'right', top: 200 }), activeView: GM_getValue('sh_activeView', 'home'), }; function saveConfig() { try { Object.keys(CONFIG).forEach(k => GM_setValue('sh_' + k, CONFIG[k])); } catch (e) {} } const log = (...a) => console.log(`[${BRAND}${IS_TOP ? '' : '·f'}]`, ...a); const sleep = ms => new Promise(r => setTimeout(r, ms)); const rand = (a, b) => a + Math.random() * (b - a); 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 []; } } // ==================== 共享题库 ==================== let SHARED_BANK = {}; async function loadSharedBank() { if (!CONFIG.enableSharedBank || !CONFIG.sharedBankUrl) return false; return new Promise(resolve => { GM_xmlhttpRequest({ method: 'GET', url: CONFIG.sharedBankUrl, timeout: 10000, onload: r => { try { const data = JSON.parse(r.responseText); SHARED_BANK = data || {}; log(`共享题库已加载:${Object.keys(SHARED_BANK).length} 题`); const el = domGet('sh-shared-count'); if (el) el.textContent = Object.keys(SHARED_BANK).length; resolve(true); } catch (e) { resolve(false); } }, onerror: () => resolve(false), ontimeout: () => resolve(false), }); }); } // ==================== 本地题库 ==================== 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 key = hashQ(t); const local = loadBank(); if (local[key]) return local[key].a; if (SHARED_BANK[key]) return SHARED_BANK[key].a; return 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 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, shared: Object.keys(SHARED_BANK).length }; } // ==================== 任务队列 ==================== let CURRENT_TASK = null; let IS_SWITCHING = false; let VIDEO_ENDED_AT = 0; let LAST_ACTION = 0; function makeTaskId(v, i) { const src = (v.currentSrc || v.src || '').slice(-60); return `t_${i}_${src}`; } function findTaskContainer(video) { let el = video; for (let i = 0; i < 8; i++) { if (!el) break; el = el.parentElement; if (!el) break; const cls = el.className || '', id = el.id || ''; if (/task[-_]?point|job[-_]?item|ans[-_]?job|video[-_]?wrap|video[-_]?item|chapter[-_]?item/i.test(cls + ' ' + id)) return el; } return video.parentElement || video; } function isGreenColor(str) { if (!str) return false; if (str.includes('green') || str.includes('success')) 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.25 && g > b * 1.25; } return false; } function isTaskCompleted(video) { if (!video) return false; const container = findTaskContainer(video); if (container) { const els = container.querySelectorAll('.icon_Completed, .ans-job-icon, [class*="complete"], [class*="finish"], [class*="done"], [class*="success"], .icon-dui, .icon-success'); for (const el of els) { const st = getComputedStyle(el); if (isGreenColor(st.color) || isGreenColor(st.backgroundColor) || isGreenColor(st.borderColor)) return true; try { const before = getComputedStyle(el, '::before'); if (isGreenColor(before.color) || isGreenColor(before.backgroundColor)) return true; } catch (e) {} } const textEls = container.querySelectorAll('span,div,em,i,p'); for (const el of textEls) { const t = el.textContent.trim(); if (/^(任务点已完成|已完成|已学完|完成学习|学习完成)$/.test(t) && isGreenColor(getComputedStyle(el).color)) return true; } } const bars = document.querySelectorAll('.ans-job-status, #videoTab, .video-tab-status, .ans-job-info'); for (const bar of bars) { if (/任务点已完成|已完成/.test(bar.innerText) && isGreenColor(getComputedStyle(bar).color)) return true; } return false; } function buildQueue() { const videos = [...document.querySelectorAll('video')]; const q = []; videos.forEach((v, i) => { const taskId = makeTaskId(v, i); if (v.dataset.shTaskDone === '1') return; if (isTaskCompleted(v)) { v.dataset.shTaskDone = '1'; return; } const r = v.getBoundingClientRect(); if (r.width < 80 || r.height < 60) return; q.push({ taskId, video: v, container: findTaskContainer(v), completed: false }); }); return q; } function pickHead() { const q = buildQueue(); return q.find(t => !t.completed && document.contains(t.video)) || null; } function markDone(task) { if (!task) return; task.completed = true; try { task.video.dataset.shTaskDone = '1'; } catch (e) {} log('✓ 任务完成:', task.taskId.slice(0, 20)); } // ==================== 播放 & 轮询 ==================== async function playVideo(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) { log('静音播放失败'); } } } } 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; } if (!CURRENT_TASK) { const head = pickHead(); if (!head) { if (IS_TOP && document.querySelectorAll('video').length > 0) { log('队列空,尝试下一节'); await goNext(); } return; } if (!acquireLock(head.video.currentSrc || '')) return; CURRENT_TASK = head; VIDEO_ENDED_AT = 0; log('▶ 开始任务:', head.taskId.slice(0, 20)); await playVideo(head.video); return; } const v = CURRENT_TASK.video; if (!document.contains(v)) { releaseTask(); return; } if (isTaskCompleted(v)) { log('✅ 任务点已变绿(提前检测)'); await finishTask(CURRENT_TASK, true); return; } const ended = v.ended || (v.duration && v.currentTime / v.duration >= 0.99); const green = isTaskCompleted(v); if (ended && !VIDEO_ENDED_AT) { VIDEO_ENDED_AT = Date.now(); updateStatus('等待任务点变绿'); log('视频结束,等待任务点变绿...'); } if (ended && green) { const now = Date.now(); if (now - LAST_ACTION < 1000) return; LAST_ACTION = now; log('✅ 双条件满足'); await finishTask(CURRENT_TASK); return; } if (ended && VIDEO_ENDED_AT && Date.now() - VIDEO_ENDED_AT > 90000) { log('⚠ 超时强制通过'); await finishTask(CURRENT_TASK, true); return; } if (v.paused && !ended) { try { await v.play(); applyRateDynamic(v, CONFIG.playbackRate); updateStatus('播放中'); } catch (e) { v.muted = true; try { await v.play(); } catch (e2) {} } } else { applyRateDynamic(v, CONFIG.playbackRate); updateStatus('播放中'); } } async function finishTask(task, force = false) { markDone(task); const delay = 3000 + Math.random() * 12000; log(`任务完成,延时 ${(delay / 1000).toFixed(1)}s`); updateStatus(`延时 ${(delay / 1000).toFixed(0)}s`); try { task.video.pause(); } catch (e) {} 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; log(`章节冷却 ${(cooldown / 60000).toFixed(1)} 分钟`); 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) }); }); } // ==================== AI 调用 ==================== async function callAI(q, opts, type) { const prov = AI_PROVIDERS[CONFIG.aiProvider]; const key = (CONFIG.aiKeys || {})[CONFIG.aiProvider]; if (!prov || !key) return null; const sys = `你是一位精通中国大学课程的资深教授。请严谨解答。`; let usr; if (type === 'blank') usr = `填空题,多空用||分隔:\n${q}\n答案:`; else if (type === 'short') usr = `简答题:\n${q}\n答案:`; else usr = `选择题,只返回选项字母:\n${q}\n${opts.map((o, i) => `${String.fromCharCode(65 + i)}. ${o}`).join('\n')}\n答案:`; if (prov.apiStyle === 'gemini') { return new Promise(res => { GM_xmlhttpRequest({ method: 'POST', url: `${prov.url}?key=${encodeURIComponent(key)}`, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ contents: [{ parts: [{ text: `${sys}\n\n${usr}` }] }], generationConfig: { temperature: 0.1, maxOutputTokens: 200 } }), timeout: 15000, onload: r => { try { const d = JSON.parse(r.responseText); const t = d.candidates[0].content.parts[0].text.trim(); res(type === 'blank' || type === 'short' ? t : t.replace(/[^A-D]/g, '')); } catch (e) { res(null); } }, onerror: () => res(null), ontimeout: () => res(null) }); }); } else { return new Promise(res => { 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: 15000, onload: r => { try { const d = JSON.parse(r.responseText); const t = d.choices[0].message.content.trim(); res(type === 'blank' || type === 'short' ? t : t.replace(/[^A-D]/g, '')); } catch (e) { res(null); } }, onerror: () => res(null), ontimeout: () => res(null) }); }); } } // ==================== 题目提取与答题 ==================== function inViewport(el) { const r = el.getBoundingClientRect(); const wh = innerHeight; const ww = innerWidth; return r.width > 0 && r.height > 0 && r.bottom > 0 && r.right > 0 && r.top < wh && r.left < ww; } function isQuestion(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']; function extractOne(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 (!isQuestion(qt)) return null; const radios = item.querySelectorAll('input[type="radio"]'); const checks = item.querySelectorAll('input[type="checkbox"]'); const texts = item.querySelectorAll('input[type="text"],textarea,.blank-input,.fill-input'); let type = 'radio', els = []; if (checks.length > 0 && radios.length === 0) { type = 'checkbox'; els = [...checks]; } else if (radios.length > 0) { type = 'radio'; els = [...radios]; } else if (texts.length > 0) { type = 'blank'; els = [...texts]; } else type = 'short'; const opts = []; if (type === 'radio' || type === 'checkbox') { els.forEach(o => { const lb = o.closest('label') || o.parentElement; if (lb) opts.push(lb.textContent.trim().replace(/^[A-D][.、\s]*/, '')); }); } return { element: item, text: qt, type, options: opts, optionEls: els }; } function isAnswered(q) { if (q.type === 'radio' || q.type === 'checkbox') return q.optionEls.some(o => o.checked); if (q.type === 'blank') return q.optionEls.some(o => (o.value || '').trim().length > 0); return false; } function isGraded(item) { const m = item.querySelector('.right,.wrong,.correct,.incorrect,.dui,.cuo,.answer-right,.answer-wrong,[class*="right-answer"],[class*="wrong-answer"],[class*="correctOption"],.icon-dui,.icon-cuo'); if (m) return true; return /(?:正确答案|参考答案|标准答案)[::]/.test(item.textContent || ''); } function extractSysAnswer(item) { const cc = item.querySelectorAll('.right,.correct,.dui,.answer-right,.correct-answer,[class*="right-answer"],[class*="correctOption"],[class*="correct-option"],.answer_right,.option-right,li.right'); if (cc.length > 0) { const ls = []; cc.forEach(el => { const le = el.querySelector('.option-letter,.letter,.num,[class*="letter"]'); if (le) { const l = le.textContent.trim().replace(/[^A-Da-d]/g, '').toUpperCase(); if (l && !ls.includes(l)) ls.push(l); return; } const cls = el.className || ''; const m = cls.match(/option[-_]?([a-dA-D])|choice[-_]?([a-dA-D])|^([a-dA-D])$/); if (m) { const l = (m[1] || m[2] || m[3] || '').toUpperCase(); if (l && !ls.includes(l)) ls.push(l); } }); if (ls.length > 0) { ls.sort(); return ls.join(''); } } const t = item.textContent || ''; const m = t.match(/(?:正确答案|参考答案|标准答案)[::]\s*([A-Da-d]{1,4})/); if (m) return m[1].toUpperCase().split('').sort().join(''); return null; } function humanClick(el) { if (!el) return; try { el.scrollIntoView({ block: 'center', behavior: 'smooth' }); } catch (e) {} const r = el.getBoundingClientRect(); const x = r.left + r.width / 2 + rand(-4, 4), y = r.top + r.height / 2 + rand(-3, 3); ['mouseover', 'mousedown', 'mouseup', 'click'].forEach(t => el.dispatchEvent(new MouseEvent(t, { bubbles: true, cancelable: true, clientX: x, clientY: y }))); el.dispatchEvent(new Event('change', { bubbles: true })); el.dispatchEvent(new Event('input', { bubbles: true })); } function humanType(el, text) { if (!el) return; try { el.scrollIntoView({ block: 'center', behavior: 'smooth' }); } catch (e) {} el.focus(); const setter = Object.getOwnPropertyDescriptor(el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype, 'value')?.set; if (setter) setter.call(el, text); else el.value = text; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } async function autoAnswer() { if (!CONFIG.enabled || !CONFIG.autoAnswer || window._shAnswering) return; window._shAnswering = true; try { for (const sel of Q_SELS) { const items = document.querySelectorAll(sel); if (!items.length) continue; for (const item of items) { if (item.dataset.shAnswered === 'done' || !inViewport(item) || item.dataset.shSubmitted === '1') continue; const q = extractOne(item); if (!q) continue; if (isAnswered(q)) { item.dataset.shSubmitted = '1'; continue; } if (window.shCollapseUI) window.shCollapseUI(); if (CONFIG.stealthMode) await sleep(rand(15000, 30000)); let ans = queryBank(q.text); let src = 'local'; if (!ans) { ans = await queryFreeBank(q.text); src = 'free'; } if (!ans) { ans = await callAI(q.text, q.options, q.type); src = 'ai'; } if (!ans) { log('❌ 无答案'); item.dataset.shSubmitted = '1'; continue; } log(`✅ ${src}: ${ans}`); recordBank(q.text, ans, q.type, src); if (CONFIG.stealthMode) await sleep(rand(8000, 15000)); if (q.type === 'radio' || q.type === 'checkbox') { const letters = ans.toUpperCase().split('').filter(c => /[A-D]/.test(c)); for (const l of letters) { const idx = l.charCodeAt(0) - 65; if (q.optionEls[idx] && !q.optionEls[idx].checked) { humanClick(q.optionEls[idx]); if (CONFIG.stealthMode) await sleep(rand(400, 900)); } } } else if (q.type === 'blank') { const answers = ans.split('||').map(s => s.trim()).filter(s => s.length > 0); for (let i = 0; i < q.optionEls.length && i < answers.length; i++) { humanType(q.optionEls[i], answers[i]); if (CONFIG.stealthMode) await sleep(rand(500, 1000)); } } else if (q.type === 'short') { const ta = q.element.querySelector('textarea,input[type="text"]'); if (ta) humanType(ta, ans); } item.dataset.shSubmitted = '1'; await sleep(rand(1000, 2000)); } break; } await sleep(2000); for (const sel of Q_SELS) { const items = document.querySelectorAll(sel); if (!items.length) continue; for (const item of items) { if (item.dataset.shAnswered === 'done' || item.dataset.shSubmitted !== '1' || !isGraded(item)) continue; const q = extractOne(item); if (!q) { item.dataset.shAnswered = 'done'; continue; } const sysAns = extractSysAnswer(item); if (sysAns) recordBank(q.text, sysAns, q.type, 'system'); item.dataset.shAnswered = 'done'; } break; } } finally { window._shAnswering = false; } } function watchQuestions() { try { new MutationObserver(() => { if (document.querySelector('.pop-question,.question-popup,.quiz-modal,[class*="popup"][class*="question"],[class*="ans-video-question"],.TiMu')) autoAnswer(); }).observe(document.body, { childList: true, subtree: true }); } catch (e) {} } // ==================== 倍速 ==================== const RATES = [1, 1.25, 1.5, 2, 2.5]; const NativeRateSetter = (() => { try { const d = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate'); return d?.set; } catch (e) { return null; } })(); const rateTargets = new WeakMap(); function forceRate(v, r) { try { NativeRateSetter ? NativeRateSetter.call(v, r) : (v.playbackRate = r); } catch (e) {} } function applyRate(v, r) { if (!v || !isFinite(r) || r <= 0) return; rateTargets.set(v, r); try { delete v.playbackRate; } catch (e) {} forceRate(v, r); if (!v.dataset.shRateLocked) { v.dataset.shRateLocked = '1'; v.addEventListener('ratechange', () => { const t = rateTargets.get(v); if (t == null) return; if (Math.abs(v.playbackRate - t) > 0.05) { setTimeout(() => { const t2 = rateTargets.get(v); if (t2 != null && Math.abs(v.playbackRate - t2) > 0.05) forceRate(v, t2); }, 0); } }, true); } } function applyRateDynamic(v, base) { if (!v) return; if (!CONFIG.stealthMode) { applyRate(v, base); return; } const jitter = rand(-0.08, 0.08); const r = Math.max(1, Math.min(4, base + jitter)); applyRate(v, r); clearTimeout(v._shJitter); v._shJitter = setTimeout(() => { if (!v.paused && !v.ended) applyRateDynamic(v, base); }, rand(20000, 45000)); } setInterval(() => { try { if (!CONFIG.enabled) return; document.querySelectorAll('video').forEach(v => { const t = rateTargets.get(v); if (t != null && Math.abs(v.playbackRate - t) > 0.05) forceRate(v, t); }); } catch (e) {} }, 2500); // ==================== 防检测 ==================== function antiDetection() { if (!CONFIG.stealthMode) return; try { Object.defineProperty(navigator, 'webdriver', { get: () => undefined, configurable: true }); } catch (e) {} try { Object.defineProperty(document, 'visibilityState', { get: () => 'visible', configurable: true }); Object.defineProperty(document, 'hidden', { get: () => false, configurable: true }); } catch (e) {} try { window.addEventListener('blur', e => e.stopImmediatePropagation(), true); } catch (e) {} try { window.addEventListener('focusout', e => e.stopImmediatePropagation(), true); } catch (e) {} try { document.hasFocus = () => true; } catch (e) {} try { window.addEventListener('mouseout', (e) => { if (!e.relatedTarget) e.stopImmediatePropagation(); }, true); } catch (e) {} try { const origAdd = EventTarget.prototype.addEventListener; EventTarget.prototype.addEventListener = function (type, listener, opts) { if (type === 'visibilitychange' && (this === document || this === window)) return; return origAdd.call(this, type, listener, opts); }; } catch (e) {} } function simulateHuman() { if (!CONFIG.stealthMode) return; setInterval(() => { try { if (Math.random() > 0.5) document.dispatchEvent(new MouseEvent('mousemove', { clientX: rand(100, innerWidth - 100), clientY: rand(100, innerHeight - 100), bubbles: true })); } catch (e) {} }, rand(8000, 20000)); setInterval(() => { try { if (Math.random() > 0.7) window.scrollBy({ top: rand(-50, 50), behavior: 'smooth' }); } catch (e) {} }, rand(20000, 40000)); } // ==================== UI ==================== const THEME = { light: { bg: '#fff', panel: '#f8faff', border: '#e5eaff', text: '#1f2328', subtext: '#57606a', hint: '#8b949e', primary: '#2f6feb', primaryLight: '#eef3ff', cardBg: '#fff', cardBorder: '#eaeef2', inputBg: '#fff', inputBorder: '#d0d7de', toggleOff: '#cfd4da', toggleOn: '#2f6feb', headerBg: 'linear-gradient(135deg,#2f6feb,#5b8dff)', danger: '#e5484d', success: '#10b981' }, dark: { bg: '#1a1d24', panel: '#0f1218', border: '#2a2f3a', text: '#e6e8eb', subtext: '#a8b0bd', hint: '#6c7480', primary: '#5b8dff', primaryLight: '#1f2a44', cardBg: '#232830', cardBorder: '#2f3540', inputBg: '#1a1d24', inputBorder: '#3a4150', toggleOff: '#3a4150', toggleOn: '#5b8dff', headerBg: 'linear-gradient(135deg,#1e4bb8,#2f6feb)', danger: '#ff6b6b', success: '#34d399' } }; function T() { return CONFIG.darkMode ? THEME.dark : THEME.light; } function updateStatus(t) { const el = domGet('sh-status-text'); if (el) el.textContent = t; } function applyBallPos() { if (!BALL_EL) return; const pos = CONFIG.ballPos || { side: 'right', top: 200 }; const top = Math.max(10, Math.min(pos.top || 200, innerHeight - 76)); if (pos.side === 'left') { BALL_EL.style.left = '20px'; BALL_EL.style.right = 'auto'; } else { BALL_EL.style.right = '20px'; BALL_EL.style.left = 'auto'; } BALL_EL.style.top = top + 'px'; if (PANEL_EL) { PANEL_EL.style.top = top + 'px'; if (pos.side === 'left') { PANEL_EL.style.left = '0'; PANEL_EL.style.right = 'auto'; PANEL_EL.style.transform = CONFIG.panelExpanded ? 'translateX(0)' : 'translateX(-110%)'; } else { PANEL_EL.style.right = '0'; PANEL_EL.style.left = 'auto'; PANEL_EL.style.transform = CONFIG.panelExpanded ? 'translateX(0)' : 'translateX(110%)'; } } } function createUI() { if (!IS_TOP) { try { document.querySelectorAll('#sh-ui-host').forEach(el => el.remove()); } catch (e) {} return; } try { document.querySelectorAll('#sh-ui-host').forEach(el => el.remove()); } catch (e) {} if (!document.body) return; const t = T(); HOST = document.createElement('div'); HOST.id = 'sh-ui-host'; HOST.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;z-index:2147483647;pointer-events:none;'; document.body.appendChild(HOST); let root; try { root = HOST.attachShadow({ mode: 'open' }); log('Shadow DOM 已启用'); } catch (e) { root = HOST; log('Shadow DOM 不可用'); } ROOT = root; const styleEl = document.createElement('style'); styleEl.textContent = ` :host{all:initial;}*{box-sizing:border-box;margin:0;padding:0;} .sh-ball{position:fixed;width:56px;height:56px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:32px;line-height:1;cursor:grab;user-select:none;pointer-events:auto;box-shadow:0 4px 14px rgba(47,111,235,0.4);font-family:-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;background:${t.headerBg};color:#fff;z-index:2147483647;transition:transform .15s;} .sh-ball:hover{transform:scale(1.08);} .sh-panel{position:fixed;width:380px;height:620px;max-height:92vh;border-radius:16px;box-shadow:0 10px 40px rgba(0,0,0,${CONFIG.darkMode?'0.55':'0.2'});display:flex;flex-direction:column;overflow:hidden;transition:transform .3s ease,opacity .3s ease;opacity:${CONFIG.panelExpanded?'1':'0'};pointer-events:${CONFIG.panelExpanded?'auto':'none'};font-family:-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;color:${t.text};background:${t.bg};border:1px solid ${t.border};z-index:2147483646;} .sh-slider::before{content:'';position:absolute;width:16px;height:16px;left:2px;top:2px;background:#fff;border-radius:50%;transition:.2s;box-shadow:0 1px 3px rgba(0,0,0,.2);} input:checked + .sh-slider::before{transform:translateX(18px);} input:checked + .sh-slider{background:${t.toggleOn} !important;} .sh-ai-option{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border:1px solid ${t.cardBorder};border-radius:8px;margin-bottom:6px;cursor:pointer;transition:all .15s;} .sh-ai-option:hover{border-color:${t.primary};} .sh-ai-option.active{border-color:${t.primary};background:${t.primaryLight};} .sh-bank-item{background:${t.cardBg};border:1px solid ${t.cardBorder};border-radius:8px;padding:10px 12px;font-size:13px;color:${t.text};} .sh-bank-item .sh-bank-q{font-weight:500;line-height:1.45;margin-bottom:4px;word-break:break-word;} .sh-bank-item .sh-bank-a{color:${t.primary};font-size:12px;margin-bottom:4px;word-break:break-word;} .sh-bank-item .sh-bank-meta{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:${t.hint};} .sh-bank-del{padding:3px 8px;border-radius:4px;border:1px solid ${t.cardBorder};background:transparent;color:${t.danger};font-size:11px;cursor:pointer;} ::-webkit-scrollbar{width:6px;}::-webkit-scrollbar-thumb{background:${t.cardBorder};border-radius:3px;} `; root.appendChild(styleEl); BALL_EL = document.createElement('div'); BALL_EL.className = 'sh-ball'; BALL_EL.textContent = '🤓'; BALL_EL.title = BRAND + '·学习助手'; BALL_EL.style.display = CONFIG.panelExpanded ? 'none' : 'flex'; root.appendChild(BALL_EL); const aiOptsHTML = Object.entries(AI_PROVIDERS).map(([k, p]) => { const active = CONFIG.aiProvider === k; const has = CONFIG.aiKeys?.[k]; return `