// ==UserScript== // @name 柳大师 - 学习助手(v10.9.0) // @namespace local.liudashi.study-helper // @version 10.9.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 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.0'; 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); // ==================== 跨 frame 锁 ==================== 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); // ==================== AI 服务商 ==================== 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"]', ]; // 课件相关选择器(含 iframe 内的) 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; } // ============================================================ // 【核心】任务类型识别:video / ppt / doc / quiz // ============================================================ function getCardType(card) { if (!card) return 'unknown'; // 1. video 优先 if (card.querySelector('video')) return 'video'; // 2. iframe 里的 video const iframes = card.querySelectorAll('iframe'); for (const f of iframes) { if (/video|media|player|ananas/i.test(f.src || '')) return 'video'; // 可能是 ppt if (PPT_FRAME_HINTS.test(f.src || '')) { // 看 iframe 里的内容 try { const idoc = f.contentDocument || f.contentWindow?.document; if (idoc && (idoc.querySelector('video'))) return 'video'; } catch (e) {} return 'ppt'; } } // 3. 文字提示 const text = card.textContent || ''; if (/测验|答题|选择题|判断题|考试|考核/.test(text)) return 'quiz'; if (/视频|视屏|课程视频|观看/.test(text)) return 'video'; if (/课件|文档|阅读|ppt|PPT|幻灯片|PPT课件/.test(text)) return 'ppt'; if (/pdf|PDF/.test(text)) return 'doc'; // 4. class 名判断 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'; 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)) { const v = card.querySelector('video'); if (v) v.dataset.shTaskDone = '1'; continue; } const type = getCardType(card); if (type === 'unknown' && !card.querySelector('video') && !card.querySelector('iframe')) { card.dataset.shSkipped = '1'; continue; } queue.push({ card, type, video: card.querySelector('video'), 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 = 3000; const QUIZ_TIMEOUT = 5 * 60 * 1000; const PPT_STALL_TIMEOUT = 90 * 1000; // 课件 90 秒无进展则视为卡死 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 findPptFrame(card) { if (!card) return null; // 优先找 src 含 ppt 的 iframe const iframes = [...card.querySelectorAll('iframe')]; for (const f of iframes) { if (PPT_FRAME_HINTS.test(f.src || '')) return f; } // 否则找第一个能访问的 iframe for (const f of iframes) { try { const d = f.contentDocument; if (d && d.body) return f; } catch (e) {} } // 没有 iframe,可能内容直接在卡片里 if (card.querySelector('[class*="slide"], [class*="ppt"], [class*="panview"]')) return null; return null; } // 尝试点击"下一页"按钮,返回是否点击成功 function tryClickPptNext(pageDoc) { if (!pageDoc) return false; // 1. 按选择器找按钮 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, view: pageDoc.defaultView || window })); return true; } catch (e) {} } } catch (e) {} } // 2. 找含"下一页"文字的按钮 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 win = pageDoc.defaultView || window; const targets = [pageDoc.body, pageDoc.documentElement].filter(Boolean); const keys = ['ArrowRight', 'PageDown', ' ', 'Right']; keys.forEach(k => { targets.forEach(t => { try { t.dispatchEvent(new KeyboardEvent('keydown', { key: k, keyCode: k === 'ArrowRight' ? 39 : k === 'PageDown' ? 34 : 32, bubbles: true })); t.dispatchEvent(new KeyboardEvent('keyup', { key: k, keyCode: k === 'ArrowRight' ? 39 : k === 'PageDown' ? 34 : 32, bubbles: true })); } catch (e) {} }); try { win.dispatchEvent(new KeyboardEvent('keydown', { key: k, bubbles: true })); win.dispatchEvent(new KeyboardEvent('keyup', { key: k, bubbles: true })); } catch (e) {} }); } // 课件任务处理逻辑,返回状态: // 'done' - 课件已完成 // 'clicked' - 翻了页,继续 // 'key' - 键盘翻页,继续 // 'wait' - 没进展,等待 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; } // 如果没 iframe,直接在卡片内搜索 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 (now - LAST_SCAN_AT > SCAN_INTERVAL) { LAST_SCAN_AT = now; if (CURRENT_TASK && CURRENT_TASK.card && isCardCompleted(CURRENT_TASK.card)) { await finishTask(CURRENT_TASK, true); return; } } // ============================================================ // 有当前任务 → 处理 // ============================================================ if (CURRENT_TASK) { const task = CURRENT_TASK; // ---- quiz 任务 ---- if (task.type === 'quiz') { if (!task.card || !document.contains(task.card)) { log('📤 quiz 卡片已移除,释放'); releaseTask(); return; } if (task.startTime && (now - task.startTime > QUIZ_TIMEOUT)) { log('⏱ quiz 超时释放'); releaseTask(); return; } if (isCardCompleted(task.card)) { await finishTask(task, true); return; } return; } // ---- ppt 任务 ---- if (task.type === 'ppt' || task.type === 'doc') { if (!task.card || !document.contains(task.card)) { log('📤 ppt 卡片已移除,释放'); releaseTask(); return; } // 每 3 秒检查一次卡片是否完成 if (isCardCompleted(task.card)) { log('✅ 课件完成'); await finishTask(task, true); return; } // 卡死检测 if (task.lastActionTime && (now - task.lastActionTime > PPT_STALL_TIMEOUT) && (now - task.startTime > PPT_STALL_TIMEOUT * 2)) { log('⚠️ 课件长时间无进展,释放任务'); releaseTask(); return; } // 每 2.5 秒尝试翻页 if (!task.lastActionTime || now - task.lastActionTime > 2500) { task.lastActionTime = now; const result = await handlePptTask(task); if (result === 'done') { await finishTask(task, true); return; } log('📄 课件翻页:', result); } updateStatus('课件翻页中'); return; } // ---- video 任务 ---- const v = task.video; if (!v || !document.contains(v)) { if (task.card && isCardCompleted(task.card)) await finishTask(task, true); else releaseTask(); return; } const cardDone = task.card && isCardCompleted(task.card); const ended = v.ended || (v.duration && v.currentTime / v.duration >= 0.99); if (ended && !VIDEO_ENDED_AT) { VIDEO_ENDED_AT = Date.now(); updateStatus('等待任务点变绿'); } if (cardDone) { const n = Date.now(); if (n - LAST_ACTION < 1000) return; LAST_ACTION = n; await finishTask(task); return; } if (ended && VIDEO_ENDED_AT && Date.now() - VIDEO_ENDED_AT > 90000) { log('⚠️ 视频超时强制通过'); await finishTask(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 if (!ended) { applyRateDynamic(v, CONFIG.playbackRate); updateStatus('播放中'); } return; } // ============================================================ // 无当前任务 → 扫描队列 // ============================================================ const queue = scanTaskCards(); if (!queue.length) { const quizCount = document.querySelectorAll('.TiMu').length; const videoCount = document.querySelectorAll('video').length; const pptCount = document.querySelectorAll('iframe').length; if (quizCount > 0 && videoCount === 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) { if (head.card) head.card.dataset.shTried = '1'; return; } if (!acquireLock(v.currentSrc || 'video')) return; CURRENT_TASK = head; CURRENT_TASK.video = v; CURRENT_TASK.startTime = Date.now(); VIDEO_ENDED_AT = 0; try { head.card.click(); } catch (e) {} await sleep(1500); await playVideoElement(v); return; } else if (head.type === 'ppt' || head.type === 'doc') { // 课件任务:点击卡片进入 CURRENT_TASK = head; CURRENT_TASK.video = null; CURRENT_TASK.startTime = Date.now(); CURRENT_TASK.lastActionTime = 0; log('📄 进入课件任务'); try { head.card.click(); } catch (e) {} await sleep(2000); // 首次尝试翻页 const result = await handlePptTask(head); if (result === 'done') await finishTask(head, true); return; } else if (head.type === 'quiz') { CURRENT_TASK = head; CURRENT_TASK.video = null; CURRENT_TASK.startTime = Date.now(); log('📝 进入测验任务'); try { head.card.click(); } catch (e) {} return; } } 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) {} } } } 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) }); }); } // ==================== AI 调用(保持严格 Prompt)==================== 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 = `你是一位答题助手。用户会给你一道判断题。必须严格遵守:\n1. 只输出一个字:"对" 或 "错"\n2. 不要输出任何解释、标点、其他文字\n3. 如果题目有乱码,根据上下文推测原题再判断\n示例输出:"对"`; usr = `判断题:${q}\n\n只回答"对"或"错":`; } else { const optText = opts.map((o, i) => `${String.fromCharCode(65 + i)}. ${o.text || o}`).join('\n'); sys = `你是一位答题助手。用户会给你一道选择题。必须严格遵守:\n1. 只输出选项字母,如 "A" 或 "AC" 或 "ABD"\n2. 不要输出任何解释、标点、其他文字\n3. 单选题只输出 1 个字母,多选题输出所有正确字母不带空格\n4. 如果题目有乱码,根据上下文推测原题再作答\n示例输出:"A"`; usr = `题目:${q}\n\n选项:\n${optText}\n\n只回答选项字母:`; } } else if (type === 'blank') { sys = `你是一位答题助手。填空题必须严格遵守:\n1. 只输出答案内容,多个空用 || 分隔\n2. 不要输出任何解释、题号、"答案:"等前缀\n3. 如果题目有乱码,根据上下文推测原题再作答\n示例输出:"北京" 或 "异步电动机||变频调速"`; 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) || /判断为错|应该是错|答案是错/.test(t)) return '错'; if (/(?:答案|正确答案)[::是\s]*对/.test(t) || /^对/.test(t) || /判断为对|应该是对|答案是对/.test(t)) return '对'; if (/[×✘]/.test(t)) return '错'; if (/[√✔]/.test(t)) return '对'; const hasErr = /错误/.test(t), hasCorr = /正确/.test(t); if (hasErr && !hasCorr) return '错'; if (hasCorr && !hasErr) 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(oc))) matched.push(idx); }); return matched; } function humanClickOption(option) { const inp = option.inputEl; const label = option.labelEl || inp.closest('label') || inp.parentElement; if (!inp) return; try { inp.scrollIntoView({ block: 'center', behavior: 'smooth' }); } catch (e) {} const target = label || inp; const rect = target.getBoundingClientRect(); const cx = rect.left + rect.width / 2 + rand(-4, 4); const cy = rect.top + rect.height / 2 + rand(-3, 3); const eventInit = { bubbles: true, cancelable: true, view: window, clientX: cx, clientY: cy }; try { inp.dispatchEvent(new MouseEvent('mouseover', eventInit)); } catch (e) {} try { inp.dispatchEvent(new MouseEvent('mousedown', eventInit)); } catch (e) {} try { inp.dispatchEvent(new MouseEvent('mouseup', eventInit)); } catch (e) {} try { inp.dispatchEvent(new MouseEvent('click', eventInit)); } catch (e) {} if (label && label !== inp) { try { label.click(); } catch (e) {} } try { inp.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} try { inp.dispatchEvent(new Event('input', { bubbles: true })); } catch (e) {} setTimeout(() => { if (inp.type === 'radio' || inp.type === 'checkbox') { if (!inp.checked) { try { inp.checked = true; inp.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} } } }, 200); } function fillBlankInput(inputEl, text) { if (!inputEl || text == null) return; try { inputEl.scrollIntoView({ block: 'center', behavior: 'smooth' }); } catch (e) {} inputEl.focus(); const proto = inputEl.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; const nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value')?.set; try { if (nativeSetter) nativeSetter.call(inputEl, String(text)); else inputEl.value = String(text); } catch (e) { inputEl.value = String(text); } inputEl.dispatchEvent(new Event('input', { bubbles: true })); inputEl.dispatchEvent(new Event('change', { bubbles: true })); try { inputEl.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: 'Enter' })); } catch (e) {} setTimeout(() => { try { inputEl.blur(); } catch (e) {} }, 100); } function isCaptchaShown() { const bt = document.body ? document.body.innerText : ''; return /9010|操作异常|请输入.*验证码|人机验证|滑动验证/.test(bt); } function tryAutoSubmit() { const sels = ['.submit', '.submitBtn', '.submit-btn', '#submitBtn', '#submit', '[class*="submit"]:not([class*="submitted"])', 'button[type="submit"]']; for (const s of sels) { const btns = document.querySelectorAll(s); for (const b of btns) { const t = (b.textContent || '').trim(); if (!t || t.length > 10) continue; if (/提交|交卷|保存/.test(t) && b.offsetParent !== null && !b.disabled) { try { b.click(); } catch (e) {} return true; } } } return false; } let CAPTCHA_PAUSED = false; async function autoAnswer(manual = false) { if (!CONFIG.enabled || (!CONFIG.autoAnswer && !manual) || window._shAnswering) return; if (CURRENT_TASK && CURRENT_TASK.type === 'video' && CURRENT_TASK.video && !CURRENT_TASK.video.paused) return; if (CURRENT_TASK && (CURRENT_TASK.type === 'ppt' || CURRENT_TASK.type === 'doc')) return; window._shAnswering = true; let totalFound = 0, totalDone = 0, totalFailed = 0; try { if (isCaptchaShown()) { if (!CAPTCHA_PAUSED) { CAPTCHA_PAUSED = true; log('🛑 检测到验证码,暂停答题'); updateStatus('请处理验证码'); } return; } else if (CAPTCHA_PAUSED) { CAPTCHA_PAUSED = false; } const itemSet = new Set(); for (const sel of Q_SELS) { try { document.querySelectorAll(sel).forEach(it => itemSet.add(it)); } catch (e) {} } const allItems = [...itemSet].filter(it => inViewport(it)); const toAnswer = []; for (const item of allItems) { if (item.dataset.shAnswered === 'done' || item.dataset.shSubmitted === '1') continue; const q = extractQuestion(item); if (!q) continue; if (q.type === 'radio' || q.type === 'checkbox') { if (q.inputEls.some(i => i.checked)) { item.dataset.shSubmitted = '1'; continue; } } else if (q.type === 'blank') { if (q.inputEls.some(i => (i.value || '').trim().length > 0)) { item.dataset.shSubmitted = '1'; continue; } } toAnswer.push({ item, q }); } totalFound = toAnswer.length; if (totalFound > 0) { log(`📝 检测到 ${totalFound} 道待答题`); updateStatus(`答题中 0/${totalFound}`); } if (!totalFound) { if (manual) alert(`扫描完成\n视口内候选:${allItems.length}\n待答题目:0`); return; } for (let i = 0; i < toAnswer.length; i++) { const { item, q } = toAnswer[i]; updateStatus(`答题中 ${i + 1}/${totalFound}`); log(`📝 [${i + 1}/${totalFound}] [${q.type}] ${q.text.slice(0, 40)}...`); if (window.shCollapseUI) window.shCollapseUI(); if (CONFIG.stealthMode) await sleep(rand(1500, 3000)); 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'; totalFailed++; continue; } log(`✅ [${src}] 最终答案: "${ans}"`); recordBank(q.text, ans, q.type, src); if (CONFIG.stealthMode) await sleep(rand(600, 1200)); if (q.type === 'radio' || q.type === 'checkbox') { const indices = parseAnswer(ans, q); if (indices.length === 0) { log('⚠️ 答案无法匹配选项:', ans); item.dataset.shSubmitted = '1'; totalFailed++; continue; } log(`📌 匹配选项: ${indices.map(i => q.options[i]?.letter || i).join(',')}`); for (const idx of indices) { const opt = q.options[idx]; if (!opt || !opt.inputEl) continue; if (q.type === 'radio' && opt.inputEl.checked) break; if (q.type === 'checkbox' && opt.inputEl.checked) continue; humanClickOption(opt); if (CONFIG.stealthMode) await sleep(rand(300, 700)); } } else if (q.type === 'blank') { const answers = String(ans).split(/\|\||,|,/).map(s => s.trim()).filter(s => s.length > 0); log(`📌 填空 ${q.inputEls.length} 空: ${answers.join(' | ')}`); for (let k = 0; k < q.inputEls.length && k < answers.length; k++) { fillBlankInput(q.inputEls[k], answers[k]); if (CONFIG.stealthMode) await sleep(rand(300, 700)); } } else if (q.type === 'short') { const ta = q.element.querySelector('textarea, input[type="text"]'); if (ta) { log(`📌 简答: ${String(ans).slice(0, 30)}`); fillBlankInput(ta, ans); } } item.dataset.shSubmitted = '1'; totalDone++; await sleep(rand(500, 1000)); } await sleep(1500); for (const { item, q } of toAnswer) { const graded = item.querySelector('.right,.wrong,.correct,.incorrect,.dui,.cuo,.answer-right,.answer-wrong,[class*="right-answer"],[class*="correctOption"],.icon-dui,.icon-cuo'); if (!graded) continue; const m = (item.textContent || '').match(/(?:正确答案|参考答案|标准答案)[::]\s*([A-Da-d]{1,4}|[√×])/); if (m) { log(`📥 系统答案: ${m[1]}`); recordBank(q.text, m[1], q.type, 'system'); } item.dataset.shAnswered = 'done'; } if (CONFIG.autoSubmit && totalDone > 0) { await sleep(rand(1500, 3000)); tryAutoSubmit(); } updateStatus(`答题完成 ${totalDone}/${totalFound}`); if (manual) alert(`答题完成\n待答:${totalFound}\n已答:${totalDone}\n失败:${totalFailed}`); } 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) {} } // ==================== MutationObserver:任务卡片变化监听 ==================== function watchTaskCards() { try { let lastTrigger = 0; const ob = new MutationObserver(() => { // 防止频繁触发 const now = Date.now(); if (now - lastTrigger < 2000) return; lastTrigger = now; // 如果当前没有任务,立即触发 pollLoop if (!CURRENT_TASK && !IS_SWITCHING) { setTimeout(() => pollLoop().catch(() => {}), 300); } }); ob.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 j = rand(-0.08, 0.08); const r = Math.max(1, Math.min(4, base + j)); 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) {} } 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)); } // ==================== 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: 100 }; const top = Math.max(10, Math.min(pos.top || 100, 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 = '30px'; if (pos.side === 'left') { PANEL_EL.style.left = '0'; PANEL_EL.style.right = 'auto'; } else { PANEL_EL.style.right = '0'; PANEL_EL.style.left = 'auto'; } } } function cleanAllUI() { try { document.querySelectorAll('#sh-ui-host').forEach(el => el.remove()); document.querySelectorAll('#sh-ball').forEach(el => el.remove()); document.querySelectorAll('#sh-sidebar').forEach(el => el.remove()); } catch (e) {} } function createUI() { if (!IS_TOP) { cleanAllUI(); return false; } cleanAllUI(); if (!document.body) return false; 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' }); } catch (e) { root = HOST; } 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:60px;height:60px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:34px;line-height:1;cursor:grab;user-select:none;pointer-events:auto;box-shadow:0 4px 16px rgba(47,111,235,0.45);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:480px;height:90vh;min-height:560px;border-radius:16px;box-shadow:0 10px 40px rgba(0,0,0,${CONFIG.darkMode?'0.55':'0.22'});display:flex;flex-direction:column;overflow:hidden;transition:transform .3s ease,opacity .3s ease;pointer-events:none;opacity:0;font-family:-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;color:${t.text};background:${t.bg};border:1px solid ${t.border};z-index:2147483646;font-size:15px;} .sh-slider::before{content:'';position:absolute;width:18px;height:18px;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(20px);} input:checked + .sh-slider{background:${t.toggleOn} !important;} .sh-ai-option{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border:1px solid ${t.cardBorder};border-radius:10px;margin-bottom:10px;cursor:pointer;} .sh-ai-option.active{border-color:${t.primary};background:${t.primaryLight};} .sh-bank-item{background:${t.cardBg};border:1px solid ${t.cardBorder};border-radius:10px;padding:14px 16px;font-size:15px;color:${t.text};} .sh-bank-item .sh-bank-q{font-weight:500;line-height:1.55;margin-bottom:8px;word-break:break-word;font-size:15px;} .sh-bank-item .sh-bank-a{color:${t.primary};font-size:15px;margin-bottom:8px;word-break:break-word;line-height:1.5;} .sh-bank-item .sh-bank-meta{display:flex;justify-content:space-between;align-items:center;font-size:12px;color:${t.hint};} .sh-bank-del{padding:5px 12px;border-radius:5px;border:1px solid ${t.cardBorder};background:transparent;color:${t.danger};font-size:13px;cursor:pointer;} ::-webkit-scrollbar{width:8px;}::-webkit-scrollbar-thumb{background:${t.cardBorder};border-radius:4px;} `; root.appendChild(styleEl); BALL_EL = document.createElement('div'); BALL_EL.className = 'sh-ball'; BALL_EL.textContent = '🤓'; BALL_EL.title = BRAND + '·学习助手'; BALL_EL.style.display = '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 `
${p.name}
${p.tip}
${has ? '✓ 已配' : '未配置'}
`; }).join(''); const greeting = getGreeting(); PANEL_EL = document.createElement('div'); PANEL_EL.className = 'sh-panel'; PANEL_EL.innerHTML = `
${BRAND}v${VERSION}
${CONFIG.darkMode ? '☀️' : '🌙'}
×
${greeting},我是${BRAND} 😎
很愉快陪你共同学习!
${CONFIG.aiEnabled ? '🤖' : '⏸'}
AI 答题 ${CONFIG.aiEnabled ? '已启用' : '已暂停'}
${CONFIG.aiEnabled ? '点击右侧按钮暂停' : '点击右侧按钮启用'}
📺
自动播放
✏️
自动答题
🔍
立即扫描题目
›
⚙️
AI 答题设置
›
📚
我的题库
0›
🔧
其他设置
›
🔔待机
`; root.appendChild(PANEL_EL); applyBallPos(); BALL_EL.style.display = 'flex'; PANEL_EL.style.opacity = '0'; PANEL_EL.style.pointerEvents = 'none'; if ((CONFIG.ballPos || {}).side === 'left') PANEL_EL.style.transform = 'translateX(-110%)'; else PANEL_EL.style.transform = 'translateX(110%)'; const dragState = { dragging: false, sx: 0, sy: 0, sl: 0, st: 0, moved: false }; BALL_EL.addEventListener('mousedown', (e) => { if (e.button !== 0) return; dragState.dragging = true; dragState.moved = false; const r = BALL_EL.getBoundingClientRect(); dragState.sx = e.clientX; dragState.sy = e.clientY; dragState.sl = r.left; dragState.st = r.top; e.preventDefault(); e.stopPropagation(); }); window.addEventListener('mousemove', (e) => { if (!dragState.dragging) return; const dx = e.clientX - dragState.sx, dy = e.clientY - dragState.sy; if (Math.abs(dx) > 3 || Math.abs(dy) > 3) dragState.moved = true; BALL_EL.style.left = Math.max(-10, Math.min(dragState.sl + dx, innerWidth - 54)) + 'px'; BALL_EL.style.top = Math.max(10, Math.min(dragState.st + dy, innerHeight - 74)) + 'px'; BALL_EL.style.right = 'auto'; e.preventDefault(); }, true); window.addEventListener('mouseup', () => { if (!dragState.dragging) return; dragState.dragging = false; const r = BALL_EL.getBoundingClientRect(); const side = r.left + r.width / 2 < innerWidth / 2 ? 'left' : 'right'; CONFIG.ballPos = { side, top: Math.max(10, Math.min(r.top, innerHeight - 74)) }; saveConfig(); applyBallPos(); }, true); BALL_EL.addEventListener('click', () => { if (dragState.moved) return; expandUI(); }); function expandUI() { BALL_EL.style.display = 'none'; PANEL_EL.style.transform = 'translateX(0)'; PANEL_EL.style.opacity = '1'; PANEL_EL.style.pointerEvents = 'auto'; } function collapseUI() { const side = (CONFIG.ballPos || {}).side || 'right'; PANEL_EL.style.transform = side === 'left' ? 'translateX(-110%)' : 'translateX(110%)'; PANEL_EL.style.opacity = '0'; PANEL_EL.style.pointerEvents = 'none'; setTimeout(() => { if (BALL_EL) BALL_EL.style.display = 'flex'; }, 300); } window.shCollapseUI = collapseUI; window.shExpandUI = expandUI; const viewsMap = { home: domGet('sh-view-home'), ai: domGet('sh-view-ai'), settings: domGet('sh-view-settings'), bank: domGet('sh-view-bank') }; function switchView(v) { Object.entries(viewsMap).forEach(([k, el]) => { if (el) el.style.display = k === v ? 'flex' : 'none'; }); if (v === 'bank') renderBankList(''); if (v === 'ai') refreshAIView(); } domGet('sh-goto-ai').addEventListener('click', () => switchView('ai')); domGet('sh-goto-bank').addEventListener('click', () => switchView('bank')); domGet('sh-goto-settings').addEventListener('click', () => switchView('settings')); domGet('sh-scan-questions').addEventListener('click', () => { autoAnswer(true).catch(e => log('扫描失败:', e)); }); domQueryAll('.sh-back').forEach(el => el.addEventListener('click', () => switchView('home'))); switchView('home'); domGet('sh-toggle-ai').addEventListener('click', () => { CONFIG.aiEnabled = !CONFIG.aiEnabled; saveConfig(); try { cleanAllUI(); } catch (e) {} ROOT = null; BALL_EL = null; PANEL_EL = null; HOST = null; createUI(); }); domGet('sh-clear-bank').addEventListener('click', () => { if (!confirm('确定清空本地题库吗?此操作不可恢复。')) return; clearBank(); refreshStats(); renderBankList(''); alert('✅ 本地题库已清空'); }); domGet('sh-theme').addEventListener('click', () => { CONFIG.darkMode = !CONFIG.darkMode; saveConfig(); try { cleanAllUI(); } catch (e) {} ROOT = null; BALL_EL = null; PANEL_EL = null; HOST = null; createUI(); }); domGet('sh-close').addEventListener('click', collapseUI); function refreshAIView() { const p = AI_PROVIDERS[CONFIG.aiProvider]; if (!p) return; const tip = domGet('sh-ai-key-tip'), inp = domGet('sh-ai-key-input'); if (tip) tip.innerHTML = `注册:${p.registerUrl}`; if (inp) inp.value = CONFIG.aiKeys?.[CONFIG.aiProvider] || ''; domQueryAll('.sh-ai-option').forEach(el => el.classList.toggle('active', el.dataset.provider === CONFIG.aiProvider)); } domQueryAll('.sh-ai-option').forEach(el => el.addEventListener('click', () => { CONFIG.aiProvider = el.dataset.provider; saveConfig(); refreshAIView(); })); domGet('sh-ai-key-save').addEventListener('click', () => { const k = domGet('sh-ai-key-input').value.trim(); if (!k) return alert('请输入 Key'); if (!CONFIG.aiKeys) CONFIG.aiKeys = {}; CONFIG.aiKeys[CONFIG.aiProvider] = k; saveConfig(); alert('已保存'); refreshAIView(); }); domGet('sh-ai-key-open').addEventListener('click', () => { const p = AI_PROVIDERS[CONFIG.aiProvider]; if (p) window.open(p.registerUrl, '_blank'); }); const bindToggle = (id, key) => { const el = domGet(id); if (!el) return; el.addEventListener('change', () => { CONFIG[key] = el.checked; saveConfig(); el.nextElementSibling.style.background = el.checked ? t.toggleOn : t.toggleOff; }); }; bindToggle('sh-enabled', 'enabled'); bindToggle('sh-stealth', 'stealthMode'); bindToggle('sh-autoAnswer', 'autoAnswer'); bindToggle('sh-enableFreeBank', 'enableFreeBank'); const homeAuto = domGet('sh-home-auto'); if (homeAuto) homeAuto.addEventListener('change', () => { CONFIG.autoPlay = homeAuto.checked; saveConfig(); homeAuto.nextElementSibling.style.background = homeAuto.checked ? t.toggleOn : t.toggleOff; }); const homeAns = domGet('sh-home-answer'); if (homeAns) homeAns.addEventListener('change', () => { CONFIG.autoAnswer = homeAns.checked; saveConfig(); homeAns.nextElementSibling.style.background = homeAns.checked ? t.toggleOn : t.toggleOff; }); domGet('sh-delay').addEventListener('change', e => { CONFIG.nextDelay = parseFloat(e.target.value) || 0; saveConfig(); }); domQueryAll('.sh-rate').forEach(btn => btn.addEventListener('click', () => { const r = parseFloat(btn.dataset.rate); CONFIG.playbackRate = r; saveConfig(); domQueryAll('.sh-rate').forEach(b => { b.style.borderColor = t.cardBorder; b.style.background = 'transparent'; b.style.color = t.subtext; }); btn.style.borderColor = t.primary; btn.style.background = t.primaryLight; btn.style.color = t.primary; document.querySelectorAll('video').forEach(v => applyRateDynamic(v, r)); if (CURRENT_TASK?.video) applyRateDynamic(CURRENT_TASK.video, r); })); const bankListEl = domGet('sh-bank-list'), bankFooterEl = domGet('sh-bank-footer'); function srcLabel(s) { if (s === 'system') return '●系统'; if (s === 'manual') return '●手动'; return '●AI'; } function typeLabel(tp) { const m = { radio: '单选', checkbox: '多选', blank: '填空', short: '简答' }; return m[tp] || tp || '—'; } function escHtml(s) { return String(s || '').replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch])); } function renderBankList(f) { const items = listBank(f || ''); bankListEl.innerHTML = ''; if (!items.length) { const d = document.createElement('div'); d.style.cssText = `text-align:center;padding:40px 16px;color:${t.hint};font-size:14px;`; d.textContent = f ? '没有匹配' : '本地题库空'; bankListEl.appendChild(d); bankFooterEl.textContent = ''; return; } const MAX = 200; items.slice(0, MAX).forEach(it => { const el = document.createElement('div'); el.className = 'sh-bank-item'; const q = it.q.length > 100 ? it.q.slice(0, 100) + '…' : it.q; const a = (it.a || '').length > 80 ? it.a.slice(0, 80) + '…' : (it.a || ''); el.innerHTML = `
${escHtml(q)}
答案:${escHtml(a)}
${srcLabel(it.src)}·${typeLabel(it.type)}
`; el.querySelector('.sh-bank-del').addEventListener('click', e => { const k = e.target.dataset.key; if (confirm('删除?')) { deleteBankEntry(k); renderBankList(f); refreshStats(); } }); bankListEl.appendChild(el); }); bankFooterEl.textContent = items.length > MAX ? `共${items.length}题,仅显示前${MAX}` : `共${items.length}题`; } const searchState = { timer: null }; domGet('sh-bank-search').addEventListener('input', e => { if (searchState.timer) clearTimeout(searchState.timer); searchState.timer = setTimeout(() => renderBankList(e.target.value), 200); }); function refreshStats() { const el = domGet('sh-home-bank-count'); const s = bankStats(); if (el) el.textContent = s.total; } refreshStats(); setInterval(refreshStats, 5000); const chatLogEl = domGet('sh-chat-log'), chatInputEl = domGet('sh-chat-input'), chatSendEl = domGet('sh-chat-send'); function appendMsg(txt, who) { const d = document.createElement('div'); d.style.cssText = who === 'user' ? `align-self:flex-end;background:${t.primary};color:#fff;padding:10px 14px;border-radius:16px 16px 4px 16px;max-width:85%;word-break:break-word;font-size:14px;line-height:1.5;` : `align-self:flex-start;background:${t.panel};color:${t.text};padding:10px 14px;border-radius:16px 16px 16px 4px;max-width:85%;word-break:break-word;font-size:14px;line-height:1.5;border:1px solid ${t.cardBorder};`; d.textContent = txt; chatLogEl.appendChild(d); chatLogEl.scrollTop = chatLogEl.scrollHeight; } async function sendChat() { const q = chatInputEl.value.trim(); if (!q) return; const p = AI_PROVIDERS[CONFIG.aiProvider]; const k = CONFIG.aiKeys?.[CONFIG.aiProvider]; if (!k) { appendMsg('请先配置 Key', 'ai'); return; } appendMsg(q, 'user'); chatInputEl.value = ''; try { const res = await new Promise(r => { GM_xmlhttpRequest({ method: 'POST', url: p.url, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${k}` }, data: JSON.stringify({ model: p.model, messages: [{ role: 'system', content: `你是${BRAND},用简体中文简洁回答` }, { role: 'user', content: q }], max_tokens: 500, temperature: 0.3 }), timeout: 15000, onload: x => { try { const d = JSON.parse(x.responseText); r(d.choices[0].message.content.trim()); } catch (e) { r('解析失败'); } }, onerror: () => r('网络错误'), ontimeout: () => r('超时') }); }); appendMsg(res, 'ai'); } catch (e) { appendMsg('错误', 'ai'); } } chatSendEl.addEventListener('click', sendChat); chatInputEl.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); } }); log('✅ UI 创建完成'); return true; } function start() { // 主轮询 1.5 秒一次 setInterval(() => { try { pollLoop().catch(() => {}); } catch (e) {} }, 1500); // 答题轮询 5 秒一次 setInterval(() => { try { autoAnswer(); } catch (e) {} }, 5000); window.addEventListener('resize', () => { try { applyBallPos(); } catch (e) {} }); window.addEventListener('beforeunload', () => { try { clearLock(); } catch (e) {} }); } function bootstrap() { log(`启动 v${VERSION}`); try { antiDetection(); } catch (e) {} try { simulateHuman(); } catch (e) {} if (IS_TOP) { let attempts = 0; const tryCreate = () => { attempts++; if (document.getElementById('sh-ui-host')) return; try { const ok = createUI(); if (!ok && attempts < 5) setTimeout(tryCreate, 1000); } catch (e) { log('❌ UI 异常:', e); if (attempts < 5) setTimeout(tryCreate, 1000); } }; tryCreate(); setTimeout(tryCreate, 500); setTimeout(tryCreate, 2000); } else { cleanAllUI(); } try { watchQuestions(); } catch (e) {} try { watchTaskCards(); } catch (e) {} start(); } function waitAndBoot() { if (document.body) { bootstrap(); return; } let tries = 0; const t = setInterval(() => { tries++; if (document.body) { clearInterval(t); bootstrap(); } else if (tries >= 60) { clearInterval(t); } }, 500); } waitAndBoot(); try { GM_registerMenuCommand(`🤖 [${BRAND}] 设置智谱免费Key`, () => { const k = prompt('粘贴智谱 GLM-4-Flash 的 API Key:', CONFIG.aiKeys?.zhipu || ''); if (k !== null) { if (!CONFIG.aiKeys) CONFIG.aiKeys = {}; CONFIG.aiKeys.zhipu = k.trim(); CONFIG.aiProvider = 'zhipu'; saveConfig(); alert('已保存'); } }); GM_registerMenuCommand(`🗑 [${BRAND}] 清空本地题库`, () => { if (!confirm('确定清空本地题库吗?')) return; clearBank(); alert('✅ 已清空'); }); GM_registerMenuCommand(`🔄 [${BRAND}] 重置悬浮球位置`, () => { CONFIG.ballPos = { side: 'right', top: 100 }; saveConfig(); if (BALL_EL) applyBallPos(); }); GM_registerMenuCommand(`🚨 [${BRAND}] 强制释放当前任务`, () => { releaseTask(); clearLock(); alert('已释放当前任务'); }); } catch (e) {} })();