// ==UserScript== // @name 龙龙学习通智慧树自动答题 // @namespace longlong-tiku // @version 1.3.35 // @description 支持【超星学习通】【智慧树】章节测验、作业、考试自动答题。内置龙龙免费题库,无需付费、无需积分。支持平台:学习通(章节测验/作业/新版旧版考试)、智慧树(作业考试/学分课/H5考试)。使用说明:安装后进入测验/考试页面右侧会出现答题面板;点击F11可切换整个面板显隐;默认已配置免费题库Token一般无需修改;可点「暂停答题」手动控制;答完后请自行检查并提交。Q站长3332075033可专门进行配置脚本适配工作(请带着课程平台地址来咨询)。交流论坛:www.qsxxw.top(免费);反馈请在脚本反馈区留言。 // @author 龙龙 // @homepageURL http://www.qsxxw.top // @tag 超星学习通 // @tag 智慧树 // @tag 自动答题 // @tag 免费题库 // @match *://*.chaoxing.com/* // @match *://*.xuexitong.com/* // @match *://*.zhihuishu.com/* // @match *://*.hiknow.com/* // @match *://studentexambaseh5.zhihuishu.com/* // @icon https://www.zjooc.cn/favicon.ico // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_getResourceText // @grant unsafeWindow // @connect tiku.bookwk.top // @connect chaoxing.com // @connect xuexitong.com // @require https://cdn.jsdelivr.net/npm/blueimp-md5@2.19.0/js/md5.min.js // @require https://cdn.jsdelivr.net/gh/photopea/Typr.js/src/Typr.js // @require https://cdn.jsdelivr.net/gh/photopea/Typr.js/src/Typr.U.js // @resource Table https://cdn.jsdelivr.net/npm/tiku-static-assets@1.0.0/cx_table.json // @run-at document-end // @license MIT // ==/UserScript== (function () { 'use strict'; function _decToken(enc, key) { try { const raw = atob(enc); let out = ''; for (let i = 0; i < raw.length; i++) { out += String.fromCharCode(raw.charCodeAt(i) ^ key.charCodeAt(i % key.length)); } return out; } catch (e) { return ''; } } const DEFAULT_TOKEN = _decToken('Ryk+SQ1gLjZCKRkNMQ1XEgY2KQEvSi5FQBwSFzIBAkQ=', 'qsxxw.top'); const API_BASE = 'http://tiku.bookwk.top/api'; // ========== 设置管理 ========== var SettingsManager = { _key: 'longlong_settings', // 默认配置(当前脚本为纯答题,视频/登录类为预留项,暂未接入实际逻辑) _defaults: { showBox: 1, // 显示脚本浮窗 handleQuiz: 1, // 测验/作业/考试自动答题 reviewMode: 0, // 复习模式(只展示答案,不自动作答) answerInterval: 2000,// 每题处理间隔(ms) fillDelay: 1500, // 每个选项/填空之间的点击间隔(ms) autoNextExam: 1, // 考试自动跳转下一题 examNextDelay: 1, // 考试跳转使用随机间隔 autoSubmit: 1, // 兼容旧配置(由 quizSubmitMode 接管) quizSubmitMode: '80', // nomove|save|10~100|force 章节测验/作业答完后的保存或提交策略 stopSecondWhenFinish: 3, // 答完后等待秒数再暂存/提交 forceSubmit: 1, // 提交时自动确认弹窗 insertAnswer: 1, // 把答案插入到题目下方 decryptFont: 1, // 学习通字体解密(题目乱码还原,影响命中率) handleVideo: 1, // 处理视频任务 handleAudio: 1, // 处理音频任务 videoMode: 'normal', // simulate=后台模拟上报, normal=前台真实播放 reportInterval: 50, // 模拟上报间隔(秒) videoRate: 1, // 视频/音频倍速(1~16,高倍速有被清空风险) videoVolume: 0, // 视频音量 0~1(0=静音,利于自动播放) videoQuizStrategy: 'random', // 视频弹题:random|ignore videoAutoNext: 1, // 视频/任务完成后自动跳转下一节 randomWorkChoice: 1, // 未搜到答案时选择题随机选一项 randomWorkComplete: 1, // 未搜到答案时填空/主观题随机填写 randomCompleteTexts: '不会,不知道,待补充', // 随机填空候选,逗号分隔 workWhenNoJob: 1, // 无黄色任务点也答章节测验 faceRecognitionPause: 1, // 检测到人脸识别时暂停 panelAvoidBlock: 1 // 面板贴边放置,减少遮挡页面点击 }, _load() { // 不缓存:每次读取最新值,保证顶层面板与 iframe 里的答题逻辑跨窗口一致 var saved = {}; try { saved = JSON.parse(GM_getValue(this._key, '{}')) || {}; } catch (e) { saved = {}; } return Object.assign({}, this._defaults, saved); }, get(k) { var v = this._load()[k]; return v === undefined ? this._defaults[k] : v; }, set(k, v) { var all = this._load(); all[k] = v; GM_setValue(this._key, JSON.stringify(all)); } }; // 跨窗口暂停标志(面板在顶层窗口,答题逻辑常在 iframe,用 GM 存储共享状态) function isPaused() { try { return GM_getValue('bookwk_paused', 0) == 1; } catch (e) { return GLOBAL.stopped; } } function setPaused(p) { try { GM_setValue('bookwk_paused', p ? 1 : 0); } catch (e) {} GLOBAL.stopped = !!p; } const GLOBAL = { index: 0, stopped: false, matched: false, _qDoc: null, _typeInputIndex: 0, delay: Number(GM_getValue('bookwk_delay', 2000)) || 2000, get fillDelay() { return Number(SettingsManager.get('fillDelay')) || 1500; }, get searchDelay() { return (Number(SettingsManager.get('answerInterval')) || 2000) / 1000; }, token: String(GM_getValue('bookwk_token', DEFAULT_TOKEN) || DEFAULT_TOKEN).trim() }; function getQuizDoc() { return GLOBAL._qDoc || document; } function refreshQuizDoc() { GLOBAL._qDoc = resolveQuizDoc(); return GLOBAL._qDoc; } // 递归收集同源可访问 document(题目可能在嵌套 iframe,答题卡在父页) function collectAccessibleDocs() { const docs = []; const seen = new Set(); const add = (doc) => { if (!doc || seen.has(doc)) return; seen.add(doc); docs.push(doc); }; const walk = (doc, depth) => { if (!doc || depth > 5) return; add(doc); doc.querySelectorAll('iframe').forEach(frame => { try { const fd = frame.contentDocument || (frame.contentWindow && frame.contentWindow.document); if (fd) walk(fd, depth + 1); } catch (e) {} }); }; try { walk(window.top.document, 0); } catch (e) { walk(document, 0); } if (!seen.has(document)) walk(document, 0); return docs; } function queryAll(sel, root) { const doc = root || getQuizDoc(); return Array.from(doc.querySelectorAll(sel)); } function resolveQuizDoc() { const selectors = '.questionLi, .TiMu, .examPaper_subject, .questionBox'; let bestDoc = null; let bestScore = -1; for (const doc of collectAccessibleDocs()) { if (!doc.querySelector(selectors)) continue; const vis = Array.from(doc.querySelectorAll('.questionLi')).filter(isVisibleEl); const score = vis.length ? vis.reduce((s, el) => s + getVisibleArea(el), 0) : doc.querySelectorAll('.questionLi, .TiMu').length; if (score > bestScore) { bestScore = score; bestDoc = doc; } } return bestDoc || document; } function cxTypeInputIndex() { if (/newMooc=true/.test(location.href) && /reVersionTestStartNew/.test(location.pathname)) return 1; return GLOBAL._typeInputIndex != null ? GLOBAL._typeInputIndex : 0; } const TYPE = { '单选题': 0, '单项选择题': 0, '单项选择': 0, '单选': 0, 'SingleChoice': 0, singlechoice: 0, '多选题': 1, '多项选择题': 1, '多项选择': 1, '多选': 1, multichoice: 1, '填空题': 2, '填空': 2, '判断题': 3, '判断': 3, '对错题': 3, Judgement: 3, bijudgement: 3, '简答题': 4, '问答题': 4, '主观题': 4, '论述题': 6, '名词解释': 5, '连线题': 11, '匹配题': 11, '完型填空': 14, '完形填空': 14, '完形填空题': 14, '阅读理解': 15 }; const CX_TYPE_CODE = { 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 4, 9: 4, 10: 4, 11: 11, 14: 14, 15: 15 }; function $(sel, root) { return (root || getQuizDoc()).querySelector(sel); } function $$(sel, root) { const doc = root || getQuizDoc(); return Array.from(doc.querySelectorAll(sel)); } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } function formatString(src) { src = String(src || ''); const div = document.createElement('div'); div.innerHTML = src; src = div.textContent || div.innerText || src; src = src.replace(/[\uff01-\uff5e]/g, ch => String.fromCharCode(ch.charCodeAt(0) - 65248)); return src.replace(/\s+/g, ' ').replace(/[“”]/g, '"').replace(/[‘’]/g, "'").replace(/。/g, '.').trim(); } function filterImgText(node) { if (!node) return ''; const clone = node.cloneNode(true); clone.querySelectorAll('img[src]').forEach(img => { const p = document.createElement('span'); p.textContent = ''; img.replaceWith(p); }); return formatString(clone.textContent || ''); } function getQuestionType(str) { if (!str) return undefined; str = String(str).trim().replace(/\s+/g, ''); if (TYPE[str] !== undefined) return TYPE[str]; const keys = Object.keys(TYPE); for (let i = 0; i < keys.length; i++) { if (str.includes(keys[i])) return TYPE[keys[i]]; } return undefined; } function isTrue(str) { const s = String(str || '').trim(); return /(^|,)(正确|是|对|√|T|true)(,|$)/i.test(s) || /^a$/i.test(s); } function isFalse(str) { const s = String(str || '').trim(); return /(^|,)(错误|否|错|×|F|false)(,|$)/i.test(s) || /^b$/i.test(s); } /** 判断题专用:题库偶发返回 C 表示「错」,仅 2 选项时按 B/错 处理 */ function resolveJudgeAnswer(raw, optionCount) { const text = String(raw || '').trim(); if (!text) return null; if (isTrue(text)) return true; if (isFalse(text)) return false; const letters = resolvePlainChoiceLetters(text); if (letters.length === 1) { const l = letters[0]; if (l === 'A') return true; if (l === 'B') return false; if (l === 'C' && optionCount <= 2) return false; } if (/^a$/i.test(text)) return true; if (/^b$/i.test(text)) return false; if (/^c$/i.test(text) && optionCount <= 2) return false; return null; } function isPlainAnswer(answer) { if (!answer || answer.length > 8 || !/[A-Z]/i.test(answer)) return false; let min = 0; for (let i = 0; i < answer.length; i++) { const c = answer.toUpperCase().charCodeAt(i); if (c < 65 || c > 71) return false; if (c < min) return false; min = c; } return true; } // ABC/OK 取长补短:相似字符归一 + NFKC + 去选项前缀 + 去标点 const SIMILAR_CHAR_MAP = { 'ɑ': 'α', 'Α': 'A', 'Β': 'B', 'Ε': 'E', 'Ζ': 'Z', 'Η': 'H', 'Ι': 'I', 'Κ': 'K', 'Μ': 'M', 'Ν': 'N', 'Ο': 'O', 'Ρ': 'P', 'Τ': 'T', 'Υ': 'Y', 'Χ': 'X', 'ο': 'o', 'ν': 'v' }; const ANSWER_SEPARATORS = ['===', '###', '---', '#', '|', '|', ';', ';', '====']; function applySimilarChars(str) { let result = String(str || ''); for (const [from, to] of Object.entries(SIMILAR_CHAR_MAP)) { result = result.split(from).join(to); } return result; } function removeOptionPrefix(text) { return String(text || '').trim() .replace(/^[A-Z][\s..、,,::))\-]+(?=\S)/i, '') .replace(/^[A-Z](?=[\u2E80-\u9FFF])/i, '') .trim(); } function normMatchText(s) { let text = formatString(s); try { text = text.normalize('NFKC'); } catch (e) {} text = applySimilarChars(text); text = removeOptionPrefix(text); return text.replace(/[\p{P}\p{S}\s]/gu, '').toLowerCase(); } // 归一化选项/答案文本(兼容 ABC normalizeAnswerForMatch 思路) function normOption(s) { return normMatchText(s); } function resolvePlainChoiceLetters(answer) { const resolved = String(answer || '').normalize('NFKC') .replace(/[,,.。.、#\s]/g, '').trim().toUpperCase(); if (!/^[A-H]+$/.test(resolved)) return []; const letters = resolved.split(''); return new Set(letters).size === letters.length ? letters : []; } /** 将选项索引统一映射到 optionNodes(optionNodes 可能是 Zy_ulTop li 的子集) */ function mapIndicesToOptionNodes(indices, optionNodes) { if (!optionNodes || !optionNodes.length) return indices || []; const max = optionNodes.length; return (indices || []).filter(i => i >= 0 && i < max); } function isTiMuAnswerBgMode(item) { if (!item) return false; const lis = collectItemOptionLis(item); if (lis.length >= 2) { return lis.some(li => li.querySelector('.answerBg')); } return item.querySelectorAll('.stem_answer .answerBg, .answerList .answerBg').length >= 2; } /** 本题选项区域(避免整题 DOM 里其它 input 干扰题型/选项采集) */ function getTiMuOptionScope(item) { if (!item) return null; return item.querySelector('.stem_answer, .answerList, .Zy_ulTop, .Cy_ulTop') || item; } function dedupeCollectedOptions(options, optionNodes) { const seen = new Set(); const opts = []; const nodes = []; const max = Math.max((options || []).length, (optionNodes || []).length); for (let i = 0; i < max; i++) { const node = optionNodes[i]; if (!node) continue; const text = (options && options[i]) || cxOptionText(node) || cxTiMuOptionText(node); const key = normOption(text); if (!key || seen.has(key)) continue; seen.add(key); opts.push(text); nodes.push(node); } return { options: opts, optionNodes: nodes }; } /** 按同一组 radio[name] 采集选项,knowledge/cards 章节测验最可靠 */ function collectItemOptionsByRadio(item) { const scope = getTiMuOptionScope(item); if (!scope) return null; const radios = Array.from(scope.querySelectorAll('input[type=radio]')); if (radios.length < 2) return null; const groups = new Map(); radios.forEach(r => { const name = (r.name || '').trim(); if (!name) return; if (!groups.has(name)) groups.set(name, []); groups.get(name).push(r); }); let bestGroup = []; groups.forEach(g => { if (g.length > bestGroup.length && g.length <= 8) bestGroup = g; }); if (bestGroup.length < 2) return null; bestGroup.sort((a, b) => { const pos = a.compareDocumentPosition(b); if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1; if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1; const av = (a.value || '').toUpperCase(); const bv = (b.value || '').toUpperCase(); if (/^[A-H]$/.test(av) && /^[A-H]$/.test(bv)) return av.charCodeAt(0) - bv.charCodeAt(0); return 0; }); const optionNodes = []; const options = []; const radiosOut = []; const seen = new Set(); bestGroup.forEach((radio, idx) => { const key = radio.name + ':' + (radio.value || idx); if (seen.has(key)) return; seen.add(key); const row = radio.closest('li') || radio.closest('.answerBg') || radio.closest('label') || radio.closest('.clearfix'); const node = (row && row.querySelector('.answerBg')) || row || radio; let t = cxOptionText(node) || (row && cxTiMuOptionText(row)) || ''; if (!t) t = radio.value || String.fromCharCode(65 + idx); options.push(t); optionNodes.push(node); radiosOut.push(radio); }); if (optionNodes.length < 2) return null; return { options, optionNodes, radios: radiosOut, mode: 'radio' }; } /** 只采集本题直属选项 li,避免嵌套/重复节点导致 8 选项、索引 4-7 错乱 */ function collectItemOptionLis(item) { if (!item) return []; const byRadio = collectItemOptionsByRadio(item); if (byRadio && byRadio.optionNodes.length >= 2) { return byRadio.optionNodes.map(n => (n.closest && n.closest('li')) || n); } const ulTop = item.querySelector('.Zy_ulTop, .Cy_ulTop'); if (ulTop) { const lis = Array.from(ulTop.children).filter(el => el.tagName === 'LI'); if (lis.length >= 2) return lis; } const stem = item.querySelector('.stem_answer, .answerList'); if (stem) { const ul = stem.querySelector(':scope > ul') || stem.querySelector('ul'); if (ul) { const lis = Array.from(ul.children).filter(el => el.tagName === 'LI'); if (lis.length >= 2) return lis; } } return []; } function recollectChoiceForItem(item) { if (!item) return null; try { decryptCxFont(); } catch (e) {} const tiMuOpts = isTiMuItem(item) ? cxCollectTiMuOptions(item) : null; if (tiMuOpts && tiMuOpts.optionNodes.length >= 2) return tiMuOpts; const lis = collectItemOptionLis(item); if (lis.length >= 2) { const options = []; const optionNodes = []; lis.forEach(li => { const node = li.querySelector('.answerBg') || li; const t = cxOptionText(node) || cxTiMuOptionText(li); if (!t) return; options.push(t); optionNodes.push(node); }); if (optionNodes.length >= 2) return { options, optionNodes }; } return null; } function getTiMuHiddenAnswerLetters(item) { if (!item) return ''; const inp = item.querySelector('.Zy_ulBottom input[name^="answer"], .Cy_ulBottom input[name^="answer"]'); const ta = item.querySelector('.Zy_ulBottom textarea[name^="answer"], .Cy_ulBottom textarea[name^="answer"]'); return ((inp && inp.value) || (ta && ta.value) || '').trim().toUpperCase(); } function verifyChoiceFilled(indices, nodes, item, scene) { indices = mapIndicesToOptionNodes(indices, nodes); if (!indices.length || !nodes.length) return false; scene = scene || getCxQuizScene(); return indices.every(i => nodes[i] && cxIgnoreClick(nodes[i], scene)); } function syncTiMuHiddenFromIndices(item, indices) { if (!item || !indices || !indices.length) return; const letters = indices.map(i => String.fromCharCode(65 + i)).join(','); updateTiMuHiddenAnswer(item, letters); } function syncTiMuHiddenFromRadio(item, radio) { if (!item || !radio) return; const scope = collectItemOptionsByRadio(item); if (!scope || !scope.radios) return; const idx = scope.radios.indexOf(radio); if (idx >= 0) updateTiMuHiddenAnswer(item, String.fromCharCode(65 + idx)); } /** 学习通答题场景(对齐万能答题 Worker 划分) */ function getCxQuizScene() { if (GLOBAL._cxScene) return GLOBAL._cxScene; const path = location.pathname; const href = location.href; if (/\/page\/quiz\/stu\/answerQuestion/.test(path)) return 'inclass'; if (/\/work\/doHomeWorkNew/.test(path) || /\/knowledge\/cards/.test(path)) return 'chapter'; if (/\/mooc2\/work\/dowork/.test(path)) return 'answerBg'; if ((/reVersionTestStartNew/.test(path) && href.includes('newMooc=true')) || /\/mooc2\/exam\/preview/.test(path) || /\/exam-ans\/mooc2\/exam\/preview/.test(path) || /\/mooc-ans\/mooc2\/exam\/preview/.test(path)) return 'answerBg'; if (/reVersionTestStartNew/.test(path) && !href.includes('newMooc=true')) return 'oldExam'; if (document.querySelector('.questionLi')) return 'answerBg'; if (document.querySelector('.TiMu')) return 'chapter'; return 'answerBg'; } /** 万能答题 ignore_click:已选中则跳过点击 */ function cxIgnoreClick(node, scene) { if (!node) return false; scene = scene || getCxQuizScene(); if (scene === 'chapter') { if (node.tagName === 'INPUT' && /radio|checkbox/i.test(node.type)) return !!node.checked; const cls = node.className || ''; if (cls.includes('check_answer')) return true; const li = node.closest && node.closest('li'); if (li) { if (li.querySelector('.check_answer, .check_answer_dx')) return true; if (/(?:^|\s)(on|cur|selected)(?:\s|$)/.test(li.className || '')) return true; } const inp = node.querySelector && node.querySelector('input[type=radio], input[type=checkbox]'); return inp ? !!inp.checked : false; } if (scene === 'oldExam') { const inp = (node.tagName === 'INPUT' && /radio|checkbox/i.test(node.type)) ? node : (node.querySelector && node.querySelector('input[type=radio], input[type=checkbox]')); return inp ? !!inp.checked : false; } const root = (node.classList && node.classList.contains('answerBg')) ? node : ((node.closest && node.closest('.answerBg')) || node); if (root.querySelector && root.querySelector('.check_answer, .check_answer_dx')) return true; const inp = root.querySelector && root.querySelector('input[type=radio], input[type=checkbox]'); if (inp && inp.checked) return true; return false; } /** 按场景采集选项(对齐万能答题 elements.$options / options) */ function cxCollectOptionsForScene(item, scene) { if (!item) return null; scene = scene || getCxQuizScene(); const cardSel = '.topicNumber_list, .NumberCardUl, .cardUl, #cardUl, .answerSheet, [class*="card"]'; if (scene === 'chapter') { const ulTop = item.querySelector('.Zy_ulTop, .Cy_ulTop'); const ul = ulTop && (ulTop.tagName === 'UL' ? ulTop : ulTop.querySelector('ul')); if (!ul) return null; const lis = Array.from(ul.children).filter(el => el.tagName === 'LI'); if (lis.length < 2) return null; const optionNodes = []; const options = []; lis.forEach(li => { const node = li.querySelector('input[type=radio], input[type=checkbox], textarea, .num_option_dx, .num_option') || li.querySelector('.after') || li; optionNodes.push(node); const after = li.querySelector('.after'); let t = after ? filterImgText(after) : cxTiMuOptionText(li); options.push(formatString(t).replace(/^[A-Ga-g][.、]/, '').trim()); }); return optionNodes.length >= 2 ? { options, optionNodes } : null; } if (scene === 'oldExam') { const inputs = Array.from(item.querySelectorAll('.Cy_ulTop input[type=radio], .Cy_ulTop input[type=checkbox]')); if (inputs.length < 2) return null; const options = inputs.map((inp, idx) => { const row = inp.closest('.clearfix') || inp.closest('li') || inp.parentElement; let t = row ? filterImgText(row).replace(/^[A-Ga-g][.、]/, '').trim() : ''; return formatString(t || inp.value || String.fromCharCode(65 + idx)); }); return { options, optionNodes: inputs }; } if (scene === 'inclass') { const list = item.querySelector('.topic-option-list'); if (!list) return null; const inputs = Array.from(list.querySelectorAll('input')); if (inputs.length < 2) return null; const options = inputs.map((inp, idx) => { const row = inp.closest('li, label, .option') || inp.parentElement; return formatString(filterImgText(row || inp).replace(/^[A-Ga-g][.、]/, '').trim() || String.fromCharCode(65 + idx)); }); return { options, optionNodes: inputs }; } let scope = item.querySelector('.stem_answer') || item; let nodes = Array.from(scope.querySelectorAll('.answerBg, .textDIV, .eidtDiv')); if (nodes.length < 2) { nodes = Array.from(item.querySelectorAll('.stem_answer .answerBg, .answerBg, .textDIV, .eidtDiv')); } nodes = nodes.filter(el => el && !el.closest(cardSel)); if (nodes.length < 2) return null; return { options: nodes.map(n => cxOptionText(n)), optionNodes: nodes }; } /** 万能答题式填充:遍历全部选项,目标状态与当前不一致才 click */ async function fillCxChoice(indices, nodes, type, item, scene) { indices = mapIndicesToOptionNodes(indices, nodes); if (!nodes.length) return false; scene = scene || (item && item._cxScene) || getCxQuizScene(); if (item) { try { item.scrollIntoView({ block: 'center', behavior: 'auto' }); } catch (e) {} await sleep(200); } for (let i = 0; i < nodes.length; i++) { const shouldSelect = indices.includes(i); const alreadySelected = cxIgnoreClick(nodes[i], scene); if (shouldSelect !== alreadySelected) { fireClick(nodes[i]); try { nodes[i].click(); } catch (e) {} await sleep(GLOBAL.fillDelay); } } if (type === 0 || type === 3) { return indices.length > 0 && indices.some(i => cxIgnoreClick(nodes[i], scene)); } return indices.length > 0 && indices.every(i => cxIgnoreClick(nodes[i], scene)); } function getOptionMatchLevel(answer, optionText) { const normalizedAnswer = normMatchText(answer); const normalizedOption = normMatchText(optionText); if (!normalizedAnswer || !normalizedOption) return 0; if (normalizedAnswer === normalizedOption) return 3; if (normalizedAnswer.includes(normalizedOption) || normalizedOption.includes(normalizedAnswer)) return 2; if (similar(normalizedAnswer, normalizedOption) >= 0.72) return 1; return 0; } function splitAnswerText(answer) { const normalizedAnswer = String(answer || '').trim(); if (!normalizedAnswer) return []; for (const separator of ANSWER_SEPARATORS) { if (normalizedAnswer.includes(separator)) { return normalizedAnswer.split(separator).map(s => s.trim()).filter(Boolean); } } return [normalizedAnswer]; } function buildAnswerGroups(answers) { if (!Array.isArray(answers)) return [splitAnswerText(answers)]; if (answers.length <= 1) return [splitAnswerText(answers[0] || '')]; const normalizedAnswers = answers.map(a => String(a || '').trim()).filter(Boolean); const groups = [normalizedAnswers]; normalizedAnswers.forEach(answer => { const split = splitAnswerText(answer); if (split.length > 1) groups.push(split); }); return groups; } // 从选项 DOM 提取正文(优先 .answer_p,与万能/DeepSeek 脚本一致) function cxOptionText(el) { if (!el) return ''; try { decryptCxFont(); } catch (e) {} const pick = (node) => { if (!node) return ''; const t = formatString((node.textContent || '').trim()); const n = normOption(t); return (n.length > 1 || /[\u4e00-\u9fff]/.test(t)) ? t : ''; }; const scopes = [el]; const bg = el.closest ? el.closest('.answerBg, li, label') : null; if (bg && bg !== el) scopes.push(bg); for (const scope of scopes) { const p = scope.querySelector('.answer_p, .answer_p_dx, .after'); const txt = pick(p); if (txt) return txt; } let text = filterImgText(el); text = text.replace(/^\s*[A-Ga-g][\s.、.。))::]+/, '') .replace(/^\s*[A-Ga-g](?=[\u4e00-\u9fff])/, '') .trim(); const n = normOption(text); if (n.length <= 1 && el.closest) { const p2 = el.closest('.answerBg, li') && el.closest('.answerBg, li').querySelector('.answer_p, .after'); const t2 = pick(p2); if (t2) return t2; } return text; } function cxOptionLetter(el, idx) { const sp = el.querySelector('.num_option, .num_option_dx, .before, [data]'); if (sp) { const l = sp.textContent.replace(/[^A-Za-z]/g, '').toUpperCase(); if (l) return l.charAt(0); } return String.fromCharCode(65 + (idx || 0)); } function normalizeType(type) { if (typeof type === 'number' && isNaN(type)) return undefined; return type; } function isChoiceType(type) { type = normalizeType(type); return type === 0 || type === 1 || type === 3 || type === undefined; } function getChoiceRoot(node) { if (!node) return null; if (node.classList && node.classList.contains('answerBg')) return node; return node.closest ? node.closest('.answerBg, li, label') : node; } // 与 DeepSeek/万能脚本一致:input.checked、aria-checked、容器/子元素 class function isChoiceChecked(node) { if (!node) return false; const findRadio = (el) => { if (!el) return null; if (el.tagName === 'INPUT' && /radio|checkbox/i.test(el.type)) return el; const inner = el.querySelector && el.querySelector('input[type=radio], input[type=checkbox]'); if (inner) return inner; const label = el.closest && el.closest('label'); if (label) return label.querySelector('input[type=radio], input[type=checkbox]'); return null; }; const radio = findRadio(node); if (radio) return !!radio.checked; const root = getChoiceRoot(node); if (!root) return false; const hasCheckedMark = (el) => { if (!el) return false; const cls = el.className || ''; if (/check_answer|check_answer_dx|onChecked|is-checked|selected|active|cur\b|highlight|selected_answer|check_box|choosen|chosen|option_checked|radio_checked/.test(cls)) return true; if (el.getAttribute && el.getAttribute('aria-checked') === 'true') return true; if (el.querySelector && el.querySelector('.check_answer, .check_answer_dx')) return true; return false; }; if (root.classList && root.classList.contains('answerBg')) { if (hasCheckedMark(root)) return true; const input = root.querySelector('input[type=radio], input[type=checkbox]'); if (input && input.checked) return true; const li = root.closest && root.closest('li'); if (li && hasCheckedMark(li)) return true; const icon = root.querySelector('.check_answer, .check_answer_dx, .icon-checkbox-checked, .icon-danxuan-checked, .icon-fuxuan-checked, [class*="checked"]'); if (icon) return true; return false; } const input = root.tagName === 'INPUT' ? root : root.querySelector('input[type=radio], input[type=checkbox]'); if (input && input.checked) return true; if (hasCheckedMark(root)) return true; return false; } async function waitChoiceChecked(node, timeoutMs) { const limit = timeoutMs != null ? timeoutMs : (GLOBAL.fillDelay || 1500); const start = Date.now(); while (Date.now() - start < limit) { if (isChoiceChecked(node)) return true; await sleep(100); } return isChoiceChecked(node); } function getVisibleArea(el) { if (!el) return 0; const r = el.getBoundingClientRect(); if (r.width <= 0 || r.height <= 0) return 0; const win = el.ownerDocument.defaultView || window; const vw = win.innerWidth || 0; const vh = win.innerHeight || 0; const x0 = Math.max(0, r.left); const x1 = Math.min(vw, r.right); const y0 = Math.max(0, r.top); const y1 = Math.min(vh, r.bottom); return Math.max(0, x1 - x0) * Math.max(0, y1 - y0); } function isVisibleEl(el) { if (!el) return false; try { const win = el.ownerDocument.defaultView || window; const s = win.getComputedStyle(el); if (s.display === 'none' || s.visibility === 'hidden' || Number(s.opacity) === 0) return false; } catch (e) {} return getVisibleArea(el) > 80; } function parseQuestionNoFromEl(item, questionEl) { const texts = []; if (item) { const h3 = item.querySelector('h3.mark_name, h3'); if (h3) texts.push(filterImgText(h3)); } if (questionEl) texts.push(filterImgText(questionEl)); for (const raw of texts) { const m = String(raw).match(/(?:^|\s)(\d+)\s*[.、.)]/); if (m) { const n = parseInt(m[1], 10); if (n > 0) return n; } } return null; } function syncTopicCardForItem(itemEl) { if (!itemEl) return; // 整页章节测验(多 TiMu 同屏):点答题卡会切换题号并取消前面已选答案 if (isChapterMultiQuiz()) return; const qId = itemEl.getAttribute('data'); for (const doc of collectAccessibleDocs()) { if (qId) { const li = doc.querySelector('.topicNumber_list li[data="' + qId + '"], .NumberCardUl li[data="' + qId + '"], .cardUl li[data="' + qId + '"], #cardUl li[data="' + qId + '"]'); if (li && !/current|active|cur/.test(li.className)) { fireClick(li); return; } } } } function isSinglePageExam() { return location.href.includes('newMooc=true') && /reVersionTestStartNew/.test(location.pathname); } function isChapterMultiQuiz() { const href = location.pathname + location.href; if (!/\/knowledge\/cards|\/work\/do|chaptertest|chapterTest/i.test(href)) return false; const doc = getQuizDoc(); return doc.querySelectorAll('.TiMu, .clearfix .TiMu').length > 1; } function getQuizItems(rootSel) { const doc = getQuizDoc(); const all = Array.from(doc.querySelectorAll(rootSel)); if (!all.length) return []; const visible = all.filter(isVisibleEl); if (!isSinglePageExam()) { if (all.length > 1 && isChapterMultiQuiz()) { return all.slice().sort((a, b) => (a.offsetTop || 0) - (b.offsetTop || 0)); } return visible.length ? visible : all; } const isCurrent = (el) => el && /(?:^|\s)(current|active|cur)(?:\s|$)/.test(el.className || ''); const cur = all.find(el => isCurrent(el) && isVisibleEl(el)); if (cur) { syncTopicCardForItem(cur); return [cur]; } if (visible.length) { let best = visible[0]; let bestArea = getVisibleArea(best); for (const el of visible) { const area = getVisibleArea(el); if (area > bestArea) { bestArea = area; best = el; } } syncTopicCardForItem(best); return [best]; } for (const d of collectAccessibleDocs()) { const cardSel = '.topicNumber_list li.current, .topicNumber_list li.active, .topicNumber_list li.cur, ' + '.NumberCardUl li.current, .cardUl li.current, .cardUl li.cur, .rightPartNumber li.current, ' + '#cardUl li.current, .kscard li.current, .sheetItem.current, .sheet-item.current, ' + '.fenye li.cur, .right_part li.cur, .sheetBox li.current, .topic-list li.current, ' + '[class*="card"] li.current, [class*="Card"] li.cur'; const cardLi = d.querySelector(cardSel); if (!cardLi) continue; const qid = cardLi.getAttribute('data'); if (qid) { const matched = all.find(el => el.getAttribute('data') === qid); if (matched) { syncTopicCardForItem(matched); return [matched]; } } const list = d.querySelectorAll('.topicNumber_list li, .NumberCardUl li, .cardUl li, #cardUl li'); const idx = Array.from(list).indexOf(cardLi); if (idx >= 0 && all[idx]) { syncTopicCardForItem(all[idx]); return [all[idx]]; } } const fallback = all[0]; syncTopicCardForItem(fallback); return [fallback]; } function optionAlreadySelected(node) { return isChoiceChecked(node); } function forceSelectInput(node, type) { const root = (node && node.classList && node.classList.contains('answerBg')) ? node : (node && node.closest ? node.closest('.answerBg') : null) || node; if (!root || !root.querySelector) return false; const input = root.querySelector('input[type=radio], input[type=checkbox]'); if (!input) return false; if (type === 0 || type === 3) { const name = input.name; const doc = root.ownerDocument || document; if (name) { doc.querySelectorAll('input[name="' + name.replace(/"/g, '\\"') + '"]').forEach(inp => { if (inp !== input) inp.checked = false; }); } } input.checked = true; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); fireClick(root); return isChoiceChecked(node); } function resolveChoiceClickTarget(node) { if (!node) return null; if (node.classList && node.classList.contains('answerBg')) return node; const bg = node.querySelector && node.querySelector('.answerBg'); if (bg) return bg; const inBg = node.closest && node.closest('.answerBg'); if (inBg) return inBg; const label = node.querySelector('label') || (node.closest && node.closest('label')); if (label) return label; const link = node.querySelector('a'); if (link) return link; return node; } function tryJqClick(el) { if (!el) return false; try { const doc = el.ownerDocument || document; const win = doc.defaultView || window; const $ = win.jQuery || win.$; if ($) { $(el).trigger('click'); return true; } } catch (e) {} return false; } function fireClick(el) { if (!el) return; const doc = el.ownerDocument || document; const win = doc.defaultView || window; try { el.scrollIntoView({ block: 'center', behavior: 'instant' }); } catch (e) {} const opts = { bubbles: true, cancelable: true, view: win }; try { el.dispatchEvent(new PointerEvent('pointerdown', opts)); el.dispatchEvent(new PointerEvent('pointerup', opts)); el.dispatchEvent(new MouseEvent('mousedown', opts)); el.dispatchEvent(new MouseEvent('mouseup', opts)); el.dispatchEvent(new MouseEvent('click', opts)); } catch (e) {} try { el.click(); } catch (e) {} tryJqClick(el); } function notifyAnswerSaved(item, type) { try { const qId = item && item.getAttribute('data'); if (!qId) return; const win = (item.ownerDocument && item.ownerDocument.defaultView) || unsafeWindow; const fn = win.loadEditorAnswered; if (typeof fn !== 'function') return; const code = type === 1 ? 1 : type === 3 ? 3 : type === 2 ? 2 : 0; fn.call(win, qId, code); } catch (e) {} } // 文本相似度(Levenshtein 归一化为 0~1),用于精确/包含都匹配不到时的兜底 function similar(a, b) { a = String(a || ''); b = String(b || ''); if (!a || !b) return 0; if (a === b) return 1; const n = a.length, m = b.length; const d = []; for (let i = 0; i <= n; i++) d[i] = [i]; for (let j = 0; j <= m; j++) d[0][j] = j; for (let i = 1; i <= n; i++) { for (let j = 1; j <= m; j++) { const cost = a.charAt(i - 1) === b.charAt(j - 1) ? 0 : 1; d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost); } } return 1 - d[n][m] / Math.max(n, m); } function searchAnswer(question) { const q = formatString(cxCleanQuestion(question)); if (!q) return Promise.resolve({ success: false, msg: '题目为空' }); const url = API_BASE + '?token=' + encodeURIComponent(GLOBAL.token) + '&q=' + encodeURIComponent(q); return new Promise(resolve => { GM_xmlhttpRequest({ method: 'GET', url, timeout: 15000, onload(res) { try { const data = JSON.parse(res.responseText); if (data.code === 1 && data.data != null && data.data !== '') { resolve({ success: true, answer: String(data.data), msg: '题库命中' }); } else { resolve({ success: false, msg: data.msg || data.message || '未找到答案' }); } } catch (e) { resolve({ success: false, msg: '接口返回解析失败' }); } }, onerror() { resolve({ success: false, msg: '题库请求失败,请检查网络或 @connect 权限' }); }, ontimeout() { resolve({ success: false, msg: '题库请求超时' }); } }); }); } function parseAnswers(raw, type, options) { const text = String(raw || '').trim(); if (!text) return []; if (type === 2 || type === 4 || type === 5 || type === 6) { const groups = buildAnswerGroups([text]); return groups[0] && groups[0].length ? groups[0] : text.split(/[||;;====]+/).map(s => s.trim()).filter(Boolean); } if (type === 3) { if (isTrue(text)) return ['正确']; if (isFalse(text)) return ['错误']; const judge = resolveJudgeAnswer(text, (options && options.length) || 2); if (judge === true) return ['正确']; if (judge === false) return ['错误']; return [text]; } const letters = resolvePlainChoiceLetters(text); if (letters.length) { return letters.map(l => { const idx = l.charCodeAt(0) - 65; return options[idx] != null ? options[idx] : l; }); } if (type === 0) return [text]; if (type === 1) { const split = splitAnswerText(text); if (split.length > 1) return split; if (/[,,、\s]+/.test(text)) { return text.split(/[,,、\s]+/).map(s => s.trim()).filter(Boolean); } } return splitAnswerText(text); } function matchOptionIndex(answerList, options, type, optionNodes) { // 判断题只按对/错语义匹配,不把 C 等字母当成第 3 个选项 if (type === 3) { const picked = new Set(); let opts = Array.isArray(options) ? options.slice() : []; if (optionNodes && optionNodes.length) { opts = optionNodes.map((n, i) => cxOptionText(n) || opts[i] || ''); } const beautiful = opts.map(normOption); const trueIdx = beautiful.findIndex(o => /正确|对|√|是|true/i.test(o)); const falseIdx = beautiful.findIndex(o => /错误|错|×|否|false/i.test(o)); for (const ans of answerList) { const judge = resolveJudgeAnswer(ans, Math.max(opts.length, 2)); if (judge === true) picked.add(trueIdx >= 0 ? trueIdx : 0); else if (judge === false) picked.add(falseIdx >= 0 ? falseIdx : (opts.length > 1 ? 1 : 0)); else if (isTrue(ans)) picked.add(trueIdx >= 0 ? trueIdx : 0); else if (isFalse(ans)) picked.add(falseIdx >= 0 ? falseIdx : (opts.length > 1 ? 1 : 0)); } const maxLen = Math.max(opts.length, optionNodes ? optionNodes.length : 0, 2); return Array.from(picked).filter(i => i >= 0 && i < maxLen); } // 始终从 DOM 重建选项文本,避免 collect 只抓到选项字母 "A" 导致匹配失败 let opts = Array.isArray(options) ? options.slice() : []; if (optionNodes && optionNodes.length) { const rebuilt = []; for (let i = 0; i < optionNodes.length; i++) { const cur = opts[i]; const fromDom = cxOptionText(optionNodes[i]); const curNorm = normOption(cur); rebuilt.push((cur && String(cur).trim().length > 1 && curNorm.length > 1) ? cur : fromDom); } opts = rebuilt; } const beautiful = opts.map(normOption); const picked = new Set(); for (const ans of answerList) { const plain = String(ans).trim(); const letters = resolvePlainChoiceLetters(plain); if (letters.length) { letters.forEach(l => { const i = l.charCodeAt(0) - 65; if (i >= 0 && i < beautiful.length) picked.add(i); }); continue; } if (isPlainAnswer(plain)) { for (let k = 0; k < plain.length; k++) { const i = plain.toUpperCase().charCodeAt(k) - 65; if (i >= 0 && i < beautiful.length) picked.add(i); } if (plain.length) continue; } const ranked = opts.map((opt, i) => ({ i, level: getOptionMatchLevel(plain, opt) })).filter(x => x.level > 0).sort((a, b) => b.level - a.level); if (ranked.length) { if (type === 1) ranked.forEach(x => picked.add(x.i)); else picked.add(ranked[0].i); continue; } const val = normMatchText(plain); if (!val) continue; let idx = beautiful.indexOf(val); if (idx < 0) idx = beautiful.findIndex(o => o && (o.includes(val) || val.includes(o))); if (idx < 0 && val.length >= 2) { let best = -1, bestScore = 0; beautiful.forEach((o, i) => { if (!o) return; const s = similar(o, val); if (s > bestScore) { bestScore = s; best = i; } }); if (bestScore >= 0.6) idx = best; } if (idx >= 0) picked.add(idx); } if (type === 3 && picked.size === 0) { const joined = answerList.join(','); const trueIdx = beautiful.findIndex(o => /正确|对|√|是|true/.test(o)); const falseIdx = beautiful.findIndex(o => /错误|错|×|否|false/.test(o)); const judge = resolveJudgeAnswer(joined, beautiful.length || 2); if (judge === true) picked.add(trueIdx >= 0 ? trueIdx : 0); else if (judge === false) picked.add(falseIdx >= 0 ? falseIdx : (beautiful.length > 1 ? 1 : 0)); else if (isTrue(joined)) picked.add(trueIdx >= 0 ? trueIdx : 0); else if (isFalse(joined)) picked.add(falseIdx >= 0 ? falseIdx : (beautiful.length > 1 ? 1 : 0)); else if (/^b$/i.test(joined)) picked.add(falseIdx >= 0 ? falseIdx : (beautiful.length > 1 ? 1 : 0)); else if (/^a$/i.test(joined)) picked.add(trueIdx >= 0 ? trueIdx : 0); } return Array.from(picked).filter(i => i >= 0 && i < Math.max(opts.length, optionNodes ? optionNodes.length : 0)); } function finalizeChoiceIndices(indices, answerList, opts, type) { let result = (indices || []).filter(i => i >= 0); if ((type === 0 || type === 3) && result.length > 1) { const joined = (answerList || []).join(' '); let best = result[0]; let bestLevel = 0; result.forEach(i => { const level = getOptionMatchLevel(joined, opts[i] || ''); if (level > bestLevel) { bestLevel = level; best = i; } }); result = [bestLevel > 0 ? best : Math.min(...result)]; } return result; } function matchOptionIndexWithFinalize(answerList, options, type, optionNodes) { const indices = matchOptionIndex(answerList, options, type, optionNodes); let opts = Array.isArray(options) ? options.slice() : []; if (optionNodes && optionNodes.length) { opts = optionNodes.map((n, i) => cxOptionText(n) || opts[i] || ''); } return finalizeChoiceIndices(indices, answerList, opts, type); } function getAnswerBgNodes(item, optionNodes) { if (optionNodes && optionNodes.length) return optionNodes; if (!item) return []; const lis = collectItemOptionLis(item); if (lis.length >= 2) { return lis.map(li => li.querySelector('.answerBg') || li); } let nodes = Array.from(item.querySelectorAll('.stem_answer .answerBg, .answerBg')); if (!nodes.length) { nodes = Array.from(item.querySelectorAll('.stem_answer li, .stem_answer label, .answerList li, .textDIV')); } return nodes.slice(0, 12); } // 万能脚本同款:仅在「应选且未选」时点击 .answerBg async function fillByWannengStyle(indices, nodes, type) { if (!nodes.length || !indices.length) return false; for (let i = 0; i < nodes.length; i++) { const shouldSelect = indices.includes(i); const already = optionAlreadySelected(nodes[i]); if (shouldSelect && !already) { const target = (nodes[i].classList && nodes[i].classList.contains('answerBg')) ? nodes[i] : (nodes[i].closest && nodes[i].closest('.answerBg')) || nodes[i]; const clickables = [target, target.querySelector && target.querySelector('.answer_p'), target.querySelector && target.querySelector('label')].filter(Boolean); for (const el of clickables) { fireClick(el); try { el.click(); } catch (e) {} tryJqClick(el); } await sleep(GLOBAL.fillDelay); } else if (!shouldSelect && already && type === 1) { fireClick(nodes[i]); await sleep(300); } } return indices.every(i => nodes[i] && isChoiceChecked(nodes[i])); } function cxClickAnswerBg(bg) { if (!bg) return; const win = (bg.ownerDocument && bg.ownerDocument.defaultView) || window; try { const $ = win.jQuery || win.$; if ($) { $(bg).trigger('click'); $(bg).click(); } } catch (e) {} const targets = [ bg.querySelector('.num_option, .num_option_dx'), bg.querySelector('.answer_p, .after'), bg.querySelector('label'), bg ].filter(Boolean); for (const el of targets) { fireClick(el); try { el.click(); } catch (e) {} tryJqClick(el); } } async function clickChoiceOption(node) { const bg = (node && node.classList && node.classList.contains('answerBg')) ? node : (node && node.closest ? node.closest('.answerBg') : null) || resolveChoiceClickTarget(node); const roots = []; const seen = new Set(); const li = bg && bg.closest ? bg.closest('li') : null; [bg, node, li].forEach(el => { if (el && !seen.has(el)) { seen.add(el); roots.push(el); } }); for (const root of roots) { cxClickAnswerBg(root.classList && root.classList.contains('answerBg') ? root : bg || root); if (await waitChoiceChecked(node || root, 800)) return true; fireClick(root); if (await waitChoiceChecked(node || root, 500)) return true; const inner = [ root.querySelector('.num_option, .num_option_dx'), root.querySelector('label'), root.querySelector('a'), root.querySelector('input[type=radio], input[type=checkbox]') ].filter(Boolean); for (const el of inner) { fireClick(el); if (await waitChoiceChecked(node || root, 600)) return true; try { el.click(); } catch (e) {} tryJqClick(el); if (await waitChoiceChecked(node || root, 400)) return true; } if (forceSelectInput(root, 0) && await waitChoiceChecked(node || root, 400)) return true; } return isChoiceChecked(node || bg); } // 参考 DeepSeek/万能脚本:遍历 .answerBg,按字母或选项正文匹配并点击 async function selectOptionLikeDeepSeek(item, rawAnswer, type, options, optionNodes) { const nodes = getAnswerBgNodes(item, optionNodes); if (!nodes.length) return { indices: [], verified: false }; const letters = String(rawAnswer || '').toUpperCase().match(/[A-G]/g); const ansList = parseAnswers(rawAnswer, type, options); const judgeTarget = type === 3 ? (isTrue(rawAnswer) ? true : isFalse(rawAnswer) ? false : null) : null; const picked = new Set(); for (let idx = 0; idx < nodes.length; idx++) { const div = nodes[idx]; const letter = cxOptionLetter(div, idx); let shouldSelect = false; if (type === 0 || type === 1) { if (letters && letters.includes(letter)) shouldSelect = true; if (!shouldSelect) { const txt = normMatchText(cxOptionText(div)); for (const ans of ansList) { if (getOptionMatchLevel(ans, cxOptionText(div)) >= 2) { shouldSelect = true; break; } const v = normMatchText(ans); if (!v) continue; if (txt === v || similar(txt, v) >= 0.6) { shouldSelect = true; break; } } } } else if (type === 3 && judgeTarget !== null) { const txt = cxOptionText(div); shouldSelect = judgeTarget ? isTrue(txt) : isFalse(txt); } if (!shouldSelect) continue; picked.add(idx); if (!isChoiceChecked(div)) { cxClickAnswerBg(div); if (!await waitChoiceChecked(div, GLOBAL.fillDelay)) { await clickChoiceOption(div); } if (!isChoiceChecked(div)) forceSelectInput(div, type); await waitChoiceChecked(div, 500); } } const indices = Array.from(picked).sort((a, b) => a - b); const verified = indices.length > 0 && indices.every(i => nodes[i] && isChoiceChecked(nodes[i])); return { indices, verified }; } async function fillChoice(indices, optionNodes, type) { if (!optionNodes || !optionNodes.length || !indices.length) return false; for (const i of indices) { const node = optionNodes[i]; if (!node || isChoiceChecked(node)) continue; await clickChoiceOption(node); if (!isChoiceChecked(node)) forceSelectInput(node, type); await waitChoiceChecked(node, 500); } return indices.every(i => optionNodes[i] && isChoiceChecked(optionNodes[i])); } function isTiMuItem(item) { return !!(item && (item.classList.contains('TiMu') || (item.closest && item.closest('.TiMu')))); } function isTiMuRowChecked(node) { if (!node) return false; const row = (node.closest && node.closest('li, dd, .clearfix')) || node; if (row.classList && /(?:^|\s)(on|cur|selected)(?:\s|$)/.test(row.className || '')) return true; const item = row.closest && row.closest('.TiMu'); if (item) { const onLi = item.querySelector('.Zy_ulTop li.on, .Cy_ulTop li.on'); if (onLi === row) return true; } const radio = row.querySelector('input[type=radio], input[type=checkbox]') || (node.tagName === 'INPUT' ? node : null); if (radio && radio.checked) return true; const span = row.querySelector('.num_option_dx, .num_option, span[data]'); if (span && /(?:^|\s)(on|cur|selected)(?:\s|$)/.test(span.className || '')) return true; if (/check_answer|check_answer_dx|selected|cur\b|on\b|active/i.test(row.className || '')) return true; const a = row.querySelector('a') || (node.tagName === 'A' ? node : null); if (a && /check_answer|check_answer_dx|selected|cur\b|on\b|active/i.test(a.className || '')) return true; const icon = row.querySelector('i[class*="checked"], i[class*="cur"], .check_answer, .check_answer_dx'); if (icon) return true; return isChoiceChecked(node); } function isTiMuHiddenFilled(item) { if (!item) return false; const inp = item.querySelector('.Zy_ulBottom input[name^="answer"], .Cy_ulBottom input[name^="answer"]'); const ta = item.querySelector('.Zy_ulBottom textarea[name^="answer"], .Cy_ulBottom textarea[name^="answer"]'); return (inp && inp.value && inp.value.trim() !== '') || (ta && ta.value && ta.value.trim() !== ''); } function getTiMuRows(item) { if (!item) return []; let rows = Array.from(item.querySelectorAll('.Zy_ulTop > li, .Cy_ulTop > li')); if (!rows.length) rows = Array.from(item.querySelectorAll('.Zy_ulTop li, .Cy_ulTop li')); if (!rows.length) { const stem = item.querySelector('.stem_answer, .answerList'); if (stem) { const ul = stem.querySelector(':scope > ul') || stem.querySelector('ul'); if (ul) rows = Array.from(ul.children).filter(el => el.tagName === 'LI'); } } if (!rows.length) { const byRadio = collectItemOptionsByRadio(item); if (byRadio) { rows = byRadio.optionNodes.map(n => (n.closest && n.closest('li')) || n); } } return rows; } function getTiMuWin(item) { return (item && item.ownerDocument && item.ownerDocument.defaultView) || unsafeWindow; } function getTiMuJq(item) { try { const win = getTiMuWin(item); return win.jQuery || win.$ || null; } catch (e) { return null; } } function updateTiMuHiddenAnswer(item, letters) { if (!item || !letters) return; const inp = item.querySelector('.Zy_ulBottom input[name^="answer"], .Cy_ulBottom input[name^="answer"]'); const ta = item.querySelector('.Zy_ulBottom textarea[name^="answer"], .Cy_ulBottom textarea[name^="answer"]'); const $ = getTiMuJq(item); if (inp) { inp.value = letters; try { inp.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} if ($) try { $(inp).val(letters).trigger('change'); } catch (e) {} } if (ta) { ta.value = letters; try { ta.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} if ($) try { $(ta).val(letters).trigger('change'); } catch (e) {} } } function clickTiMuLiOption(item, index, isSingle, notifySave) { if (notifySave === undefined) notifySave = true; const rows = getTiMuRows(item); const li = rows[index]; if (!li) return false; if (isTiMuRowChecked(li)) return true; const win = getTiMuWin(item); const $ = getTiMuJq(item); if (isSingle !== false) { rows.forEach(r => r.classList.remove('on', 'cur', 'selected')); if ($) try { $(rows).removeClass('on cur selected'); } catch (e) {} } li.classList.add('on'); if ($) try { $(li).addClass('on'); } catch (e) {} li.querySelectorAll('input[type=radio], input[type=checkbox]').forEach(inp => { inp.checked = true; try { inp.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} }); const picked = []; item.querySelectorAll('.Zy_ulTop li.on, .Cy_ulTop li.on').forEach(onLi => { const idx = rows.indexOf(onLi); if (idx >= 0) picked.push(String.fromCharCode(65 + idx)); }); if (!picked.length) picked.push(String.fromCharCode(65 + index)); updateTiMuHiddenAnswer(item, picked.join(',')); const a = li.querySelector('a'); [a, li].filter(Boolean).forEach(el => { fireClick(el); try { el.click(); } catch (e) {} if ($) try { $(el).click(); } catch (e) {} try { el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: win })); } catch (e) {} }); if (notifySave) notifyTiMuSaved(item, li.querySelector('input[type=radio], input[type=checkbox]')); return isTiMuRowChecked(li); } function selectTiMuJudge(item, answer) { const rows = getTiMuRows(item); if (!rows.length) return false; const judge = resolveJudgeAnswer(answer, rows.length); if (judge !== null) { const wantTrue = judge; const pickRow = (li) => { if (!li) return; const win = getTiMuWin(item); const $ = getTiMuJq(item); rows.forEach(r => r.classList.remove('on', 'cur', 'selected')); if ($) try { $(rows).removeClass('on cur selected'); } catch (e) {} li.classList.add('on'); if ($) try { $(li).addClass('on'); } catch (e) {} li.querySelectorAll('input[type=radio], input[type=checkbox]').forEach(inp => { inp.checked = true; try { inp.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} }); const idx = rows.indexOf(li); if (idx >= 0) updateTiMuHiddenAnswer(item, String.fromCharCode(65 + idx)); const a = li.querySelector('a'); [a, li].filter(Boolean).forEach(el => { fireClick(el); try { el.click(); } catch (e) {} if ($) try { $(el).click(); } catch (e) {} try { el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: win })); } catch (e) {} }); }; let clicked = false; rows.forEach(li => { const valAttr = (li.querySelector('span') && li.querySelector('span').getAttribute('val-param')) || li.getAttribute('val-param'); if (wantTrue && valAttr === 'true') { pickRow(li); clicked = true; } if (!wantTrue && valAttr === 'false') { pickRow(li); clicked = true; } }); if (!clicked) { rows.forEach(li => { const t = cxTiMuOptionText(li); if (wantTrue && /^对|正确|是|√|true$/i.test(t)) { pickRow(li); clicked = true; } if (!wantTrue && /^错|错误|否|×|false$/i.test(t)) { pickRow(li); clicked = true; } }); } if (!clicked && rows.length > 1) { pickRow(wantTrue ? rows[0] : rows[1]); clicked = true; } else if (!clicked && rows.length === 1) { pickRow(rows[0]); clicked = true; } if (clicked) { const picked = rows[wantTrue ? 0 : Math.min(1, rows.length - 1)]; notifyTiMuSaved(item, picked && picked.querySelector('input[type=radio], input[type=checkbox]')); } return clicked && isTiMuRowChecked(rows[wantTrue ? 0 : Math.min(1, rows.length - 1)]); } const letters = resolvePlainChoiceLetters(answer); if (letters.length === 1) { const idx = letters[0].charCodeAt(0) - 65; if (idx >= 0 && idx < rows.length) { return clickTiMuLiOption(item, idx, true, true); } } const ansLower = String(answer || '').trim().toLowerCase(); const wantTrue = isTrue(answer) || /^(a|对|正确|是|√|true|t|ri|right)$/.test(ansLower); const pickRow = (li) => { if (!li) return; const win = getTiMuWin(item); const $ = getTiMuJq(item); rows.forEach(r => r.classList.remove('on', 'cur', 'selected')); if ($) try { $(rows).removeClass('on cur selected'); } catch (e) {} li.classList.add('on'); if ($) try { $(li).addClass('on'); } catch (e) {} li.querySelectorAll('input[type=radio], input[type=checkbox]').forEach(inp => { inp.checked = true; try { inp.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} }); const idx = rows.indexOf(li); if (idx >= 0) updateTiMuHiddenAnswer(item, String.fromCharCode(65 + idx)); const a = li.querySelector('a'); [a, li].filter(Boolean).forEach(el => { fireClick(el); try { el.click(); } catch (e) {} if ($) try { $(el).click(); } catch (e) {} try { el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: win })); } catch (e) {} }); notifyTiMuSaved(item, li.querySelector('input[type=radio], input[type=checkbox]')); }; let clicked = false; rows.forEach(li => { const valAttr = (li.querySelector('span') && li.querySelector('span').getAttribute('val-param')) || li.getAttribute('val-param'); if (wantTrue && valAttr === 'true') { pickRow(li); clicked = true; } if (!wantTrue && valAttr === 'false') { pickRow(li); clicked = true; } }); if (!clicked) { rows.forEach(li => { const t = cxTiMuOptionText(li); if (wantTrue && /^对|正确|是|√|true$/i.test(t)) { pickRow(li); clicked = true; } if (!wantTrue && /^错|错误|否|×|false$/i.test(t)) { pickRow(li); clicked = true; } }); } if (!clicked && rows.length > 1) { pickRow(wantTrue ? rows[0] : rows[1]); clicked = true; } else if (!clicked && rows.length === 1) { pickRow(rows[0]); clicked = true; } return clicked && isTiMuRowChecked(rows[wantTrue ? 0 : Math.min(1, rows.length - 1)]); } function cxTiMuOptionText(row) { if (!row) return ''; const a = row.querySelector('a, label'); let t = filterImgText(a || row); t = t.replace(/^[A-Ga-g][.、\s]*/, '').replace(/^[A-Ga-g](?=[\u4e00-\u9fff])/, '').trim(); if (t.length <= 6) return t; const spans = row.querySelectorAll('span:not(.num_option):not(.num_option_dx)'); for (const sp of spans) { const s = filterImgText(sp).replace(/^[A-Ga-g][.、\s]*/, '').trim(); if (s && s.length <= 6) return s; } return t; } function cxCollectTiMuOptions(item) { const byRadio = collectItemOptionsByRadio(item); if (byRadio && byRadio.optionNodes.length >= 2) { const deduped = dedupeCollectedOptions(byRadio.options, byRadio.optionNodes); if (deduped.optionNodes.length >= 2) { return { ...deduped, mode: 'radio', radios: byRadio.radios }; } } const lis = collectItemOptionLis(item); if (lis.length >= 2) { const options = []; const optionNodes = []; const seen = new Set(); lis.forEach((li) => { const node = li.querySelector('.answerBg') || li; const t = li.querySelector('.answerBg') ? cxOptionText(node) : cxTiMuOptionText(li); const key = normOption(t); if (!t || (key && seen.has(key))) return; if (key) seen.add(key); options.push(t); optionNodes.push(node); }); if (optionNodes.length >= 2) { const mode = optionNodes.some(n => n.classList && n.classList.contains('answerBg')) ? 'answerBg' : 'ulTop'; return { options, optionNodes, mode }; } } const cyTop = item.querySelector('.Cy_ulTop, .Zy_ulTop'); if (!cyTop) return null; const optionNodes = []; const options = []; const seen = new Set(); cyTop.querySelectorAll('li, dd, .clearfix').forEach(row => { if (!row || seen.has(row)) return; seen.add(row); const t = cxTiMuOptionText(row); if (!t) return; optionNodes.push(row); options.push(t); }); return optionNodes.length ? { options, optionNodes, mode: 'ulTop' } : null; } function notifyTiMuSaved(item, radio) { try { const win = (item && item.ownerDocument && item.ownerDocument.defaultView) || unsafeWindow; if (typeof win.saveWork === 'function') win.saveWork(); if (typeof win.saveSingle === 'function') win.saveSingle(item); if (radio && typeof win.saveTiMuAnswer === 'function') win.saveTiMuAnswer(radio); } catch (e) {} } function notifyTiMuWorkSaved(item) { try { const win = (item && item.ownerDocument && item.ownerDocument.defaultView) || unsafeWindow; if (typeof win.saveWork === 'function') win.saveWork(); if (typeof win.saveSingle === 'function') win.saveSingle(item); } catch (e) {} } /** 共享课:统一点击入口 */ async function fillTiMuAnswerBg(indices, nodes, type, item) { return fillCxChoice(indices, nodes, type, item); } // 旧版章节测验 TiMu(参考 OK 助手 clickPCOption / selectPCJudge) async function fillTiMuChoice(indices, optionNodes, type, item, rawAnswer) { if (!item) return false; const nodes = optionNodes && optionNodes.length ? optionNodes : getAnswerBgNodes(item, null); if (type === 3) { if (isTiMuAnswerBgMode(item) || (nodes[0] && nodes[0].classList && nodes[0].classList.contains('answerBg'))) { const opts = nodes.map(n => cxOptionText(n)); const idx = mapIndicesToOptionNodes( matchOptionIndex(parseAnswers(rawAnswer, 3, opts), opts, 3, nodes), nodes ); return fillTiMuAnswerBg(idx, nodes, 3, item); } const ok = selectTiMuJudge(item, rawAnswer || ''); await sleep(GLOBAL.fillDelay); return ok; } indices = mapIndicesToOptionNodes(indices, optionNodes); if (!indices.length) return false; if (isTiMuAnswerBgMode(item) || (optionNodes[0] && optionNodes[0].classList && optionNodes[0].classList.contains('answerBg'))) { return fillCxChoice(indices, optionNodes, type, item); } const rows = getTiMuRows(item); const isMulti = type === 1; for (const i of indices) { const row = optionNodes[i] || rows[i]; const rowIdx = row ? rows.indexOf(row) : i; const idx = rowIdx >= 0 ? rowIdx : i; if (idx < 0 || idx >= rows.length) continue; clickTiMuLiOption(item, idx, !isMulti, true); await sleep(GLOBAL.fillDelay); } if (isMulti && indices.length) { const letters = indices.map(i => { const row = optionNodes[i] || rows[i]; const rowIdx = row ? rows.indexOf(row) : i; return String.fromCharCode(65 + (rowIdx >= 0 ? rowIdx : i)); }).join(','); updateTiMuHiddenAnswer(item, letters); notifyTiMuWorkSaved(item); } return verifyChoiceFilled(indices, optionNodes.length ? optionNodes : rows, item); } function isCxExamCtx(item) { if (/reVersionTestStartNew|exam\/preview|mooc2\/exam|exam-ans\/exam/.test(location.pathname)) return true; if (item && item.querySelector('span[data="true"], span[data="false"], span[data=\'true\'], span[data=\'false\']')) return true; return false; } function getCxChoiceContainer(optionElement) { if (!optionElement) return null; if (optionElement.classList && optionElement.classList.contains('answerBg')) return optionElement; return (optionElement.closest && optionElement.closest('.answerBg')) || optionElement; } function isCxChoiceSelected(optionElement) { const container = getCxChoiceContainer(optionElement); if (!container) return isChoiceChecked(optionElement); if (container.querySelector && container.querySelector('.check_answer, .check_answer_dx')) return true; if (container.getAttribute && container.getAttribute('aria-checked') === 'true') return true; return isChoiceChecked(container) || isChoiceChecked(optionElement); } async function clickCxChoiceContainer(container) { if (!container) return false; if (isCxChoiceSelected(container)) return true; const span = container.querySelector('span[data]'); if (span) { fireClick(span); try { span.click(); } catch (e) {} if (await waitChoiceChecked(container, 600)) return true; } cxClickAnswerBg(container); if (await waitChoiceChecked(container, GLOBAL.fillDelay)) return true; return await clickChoiceOption(container); } async function selectCxJudgeOption(item, rawAnswer, optionNodes, scene) { if (!item) return false; const nodes = optionNodes && optionNodes.length ? optionNodes : []; if (!nodes.length) return false; scene = scene || getCxQuizScene(); const opts = nodes.map((n, i) => cxOptionText(n) || (isTiMuItem(item) ? cxTiMuOptionText(n) : '') || String.fromCharCode(65 + i)); const indices = mapIndicesToOptionNodes( matchOptionIndexWithFinalize(parseAnswers(rawAnswer, 3, opts), opts, 3, nodes), nodes ); if (!indices.length) return false; return fillCxChoice(indices, nodes, 3, item, scene); } function collectBlankInputs(item) { if (!item) return []; const seen = new Set(); const inputs = []; const push = (el) => { if (!el || seen.has(el)) return; seen.add(el); inputs.push(el); }; item.querySelectorAll('textarea, input[type=text], input[type=search], input:not([type])').forEach(el => { if (el.type === 'hidden' || el.type === 'radio' || el.type === 'checkbox') return; push(el); }); item.querySelectorAll('.eidtDiv, .eidt_div, .blank, .fill-blank, .gapfilling, [contenteditable="true"]').forEach(push); if (!inputs.length) { item.querySelectorAll('.stem_answer textarea, .answerList textarea, .questionOptions textarea').forEach(push); } return inputs; } function setInputValue(el, value, win) { if (!el) return false; const text = String(value == null ? '' : value); if (el.isContentEditable) { el.innerHTML = text; el.textContent = text; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); return true; } if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') { if (win && win.UE && el.name) { try { win.UE.getEditor(el.name).setContent(text); return true; } catch (e) {} } el.value = text; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); try { const $ = win && (win.jQuery || win.$); if ($) $(el).val(text).trigger('input').trigger('change'); } catch (e) {} return true; } return false; } async function fillBlankEnhanced(answers, item, type, fillFn) { const inputs = collectBlankInputs(item); if (!inputs.length) return false; const groups = buildAnswerGroups(answers); const win = (item.ownerDocument && item.ownerDocument.defaultView) || unsafeWindow; for (const group of groups) { if (!group.length) continue; if (type === 2 && group.length !== inputs.length && group.length > 1) continue; let filled = 0; for (let i = 0; i < group.length && i < inputs.length; i++) { const el = inputs[i]; if (!el) continue; if (typeof fillFn === 'function') { await fillFn(group[i], el); } else if (!setInputValue(el, group[i], win)) { continue; } filled++; await sleep(GLOBAL.fillDelay); } if (filled > 0) return true; } const fallback = answers[0] || ''; if (fallback && setInputValue(inputs[0], fallback, win)) return true; return false; } async function fillSubjectiveAnswer(answers, item, type, fillFn) { const inputs = collectBlankInputs(item); const win = (item.ownerDocument && item.ownerDocument.defaultView) || unsafeWindow; const content = type === 2 ? answers : [answers.join('\n')]; if (!inputs.length) { if (win && win.UE) { const ta = item.querySelector('textarea[name]'); if (ta && ta.name) { try { win.UE.getEditor(ta.name).setContent(content[0] || ''); return true; } catch (e) {} } } return false; } return fillBlankEnhanced(content, item, type, fillFn); } function findElementByAttr(root, selector, attrName, attrValue) { if (!root) return null; return Array.from(root.querySelectorAll(selector)).find( el => el.getAttribute(attrName) === attrValue ) || null; } async function fillLineQuestion(item, rawAnswers) { const answers = Array.isArray(rawAnswers) ? rawAnswers : [rawAnswers]; const answerGroups = buildAnswerGroups(answers); const selectBoxes = item.querySelectorAll('.line_answer_ct .selectBox, .thirdUlList .dept_select'); const answerInputs = item.querySelectorAll('.line_answer input[name^="answer"]'); if (!selectBoxes.length) return false; for (const group of answerGroups) { if (!group.length) continue; if (answerInputs.length && group.length !== selectBoxes.length) continue; if (!answerInputs.length && group.length !== selectBoxes.length) continue; let filledCount = 0; selectBoxes.forEach((box, index) => { const ans = group[index]; if (!ans) return; const optionItem = findElementByAttr(box, 'li[data]', 'data', ans); const optionLink = optionItem && optionItem.querySelector('a'); if (optionLink) { fireClick(optionLink); try { optionLink.click(); } catch (e) {} filledCount++; return; } const selectOption = findElementByAttr(box, 'option[value]', 'value', ans); if (!selectOption) return; const prev = box.querySelector('option[selected]'); if (prev) prev.removeAttribute('selected'); selectOption.setAttribute('selected', ''); box.value = ans; const selectedText = box.parentElement && box.parentElement.querySelector('.chosen-single span'); if (selectedText) selectedText.textContent = ans; filledCount++; }); if (filledCount === selectBoxes.length) return true; } return false; } async function fillCompositeChoiceQuestion(item, rawAnswers, containerSelector) { const answers = Array.isArray(rawAnswers) ? rawAnswers : [rawAnswers]; const answerGroups = buildAnswerGroups(answers); const containers = item.querySelectorAll(containerSelector); if (!containers.length) return false; for (const group of answerGroups) { if (!group.length) continue; let filledCount = 0; group.forEach((ans, index) => { const container = containers[index]; if (!container || !ans) return; let option = findElementByAttr(container, 'span.saveSingleSelect[data]', 'data', ans); if (!option) { option = Array.from(container.querySelectorAll('span.saveSingleSelect, span[data]')).find(sp => { const data = sp.getAttribute('data'); return data === ans || normMatchText(sp.textContent) === normMatchText(ans); }) || null; } if (!option) return; fireClick(option); try { option.click(); } catch (e) {} filledCount++; }); if (filledCount > 0) return true; } return false; } // 章节测验 TiMu:.Cy_ulTop label / .clearfix + radio 填选项 async function fillLegacyChoice(indices, optionNodes, type) { if (!optionNodes || !optionNodes.length || !indices.length) return false; for (const i of indices) { const node = optionNodes[i]; if (!node || isChoiceChecked(node)) continue; const label = (node.closest && node.closest('label')) || node.querySelector('label') || node; let radio = node.querySelector('input[type=radio], input[type=checkbox]'); if (!radio && label) radio = label.querySelector('input[type=radio], input[type=checkbox]'); const targets = [label, radio, node].filter(Boolean); const seen = new Set(); for (const el of targets) { if (seen.has(el)) continue; seen.add(el); fireClick(el); try { el.click(); } catch (e) {} tryJqClick(el); } if (radio) { if (type === 0 || type === 3) { const name = radio.name; const doc = radio.ownerDocument || document; if (name) { doc.querySelectorAll('input[name="' + name.replace(/"/g, '\\"') + '"]').forEach(inp => { if (inp !== radio) inp.checked = false; }); } } radio.checked = true; radio.dispatchEvent(new Event('input', { bubbles: true })); radio.dispatchEvent(new Event('change', { bubbles: true })); radio.dispatchEvent(new Event('click', { bubbles: true })); } await sleep(GLOBAL.fillDelay); if (!isChoiceChecked(node)) await clickChoiceOption(node); await waitChoiceChecked(node, 600); } return indices.every(i => optionNodes[i] && isChoiceChecked(optionNodes[i])); } async function retryChoiceByAnswers(answers, optionNodes) { for (const ans of answers) { const val = normMatchText(ans); if (!val || val.length < 2) continue; for (let i = 0; i < optionNodes.length; i++) { if (getOptionMatchLevel(ans, cxOptionText(optionNodes[i])) >= 1) { if (!isChoiceChecked(optionNodes[i])) await clickChoiceOption(optionNodes[i]); } } } } async function fillBlank(answers, inputs, fillFn) { if (!inputs || !inputs.length || !answers || !answers.length) return false; const list = answers.length > inputs.length ? answers.slice(0, inputs.length) : answers; let filled = 0; for (let i = 0; i < list.length; i++) { const el = inputs[i]; if (!el) continue; if (typeof fillFn === 'function') { await fillFn(list[i], el); filled++; } else if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') { el.value = list[i]; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); if (String(el.value || '').trim()) filled++; } await sleep(GLOBAL.fillDelay); } return filled >= list.length; } // ========== 学习通字体解密 ========== // 原理:超星把题目文字用自定义乱码字体渲染,直接取文本会得到乱码,搜不到答案。 // 解析 woff 字形 -> md5(字形路径) -> 查预置码表 -> 还原为真实汉字。 // Typr / md5 / 码表 均由 @require、@resource 提供(jsdelivr 托管)。 function _getTypr() { if (typeof Typr !== 'undefined' && Typr && Typr.U) return Typr; try { if (unsafeWindow.Typr && unsafeWindow.Typr.U) return unsafeWindow.Typr; } catch (e) {} return null; } function _getMd5() { if (typeof md5 === 'function') return md5; try { if (typeof unsafeWindow.md5 === 'function') return unsafeWindow.md5; } catch (e) {} return null; } const _cxFont = { table: null, tableTried: false, lastFontB64: '', map: null }; function _getCxTable() { if (_cxFont.tableTried) return _cxFont.table; _cxFont.tableTried = true; try { const txt = GM_getResourceText('Table'); _cxFont.table = txt ? JSON.parse(txt) : null; } catch (e) { _cxFont.table = null; } return _cxFont.table; } function _b64ToBytes(b64) { const raw = atob(b64); const arr = new Uint8Array(raw.length); for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i); return arr; } function _extractCxFontB64() { for (const doc of collectAccessibleDocs()) { const styles = Array.from(doc.querySelectorAll('style')); for (let i = 0; i < styles.length; i++) { const s = styles[i].textContent || ''; if (s.indexOf('font-cxsecret') < 0) continue; const m = s.match(/base64,([\w\W]+?)['")]/); if (m && m[1]) return m[1].trim(); } } return null; } function _buildCxFontMap() { const Typr_ = _getTypr(); const md5_ = _getMd5(); const table = _getCxTable(); if (!Typr_ || !md5_ || !table) return null; const b64 = _extractCxFontB64(); if (!b64) return null; if (_cxFont.lastFontB64 === b64 && _cxFont.map) return _cxFont.map; let fonts; try { fonts = Typr_.parse(_b64ToBytes(b64)); } catch (e) { return null; } if (!fonts || !fonts.length) return null; const font = fonts[0]; const map = {}; // 超星乱码复用 CJK 区间(19968~40869) for (let code = 19968; code < 40870; code++) { const g = Typr_.U.codeToGlyph(font, code); if (!g) continue; const path = Typr_.U.glyphToPath(font, g); if (!path) continue; const hash = md5_(JSON.stringify(path)); if (!hash) continue; const real = table[hash.slice(24)]; if (real !== undefined && real !== null && real !== 0) map[code] = real; } _cxFont.lastFontB64 = b64; _cxFont.map = map; return map; } function decryptCxFont() { if (!SettingsManager.get('decryptFont')) return; const host = location.host; if (host.indexOf('chaoxing') < 0 && host.indexOf('xuexitong') < 0) return; const map = _buildCxFontMap(); if (!map) return; for (const doc of collectAccessibleDocs()) { const els = doc.querySelectorAll('.font-cxsecret'); if (!els.length) continue; els.forEach(el => { const ownerDoc = el.ownerDocument || doc; const walker = ownerDoc.createTreeWalker(el, NodeFilter.SHOW_TEXT, null); const nodes = []; let n; while ((n = walker.nextNode())) nodes.push(n); nodes.forEach(tn => { const s = tn.nodeValue; let out = ''; let changed = false; for (let i = 0; i < s.length; i++) { const c = s.charCodeAt(i); if (map[c] !== undefined) { out += String.fromCharCode(map[c]); changed = true; } else out += s[i]; } if (changed) tn.nodeValue = out; }); el.classList.remove('font-cxsecret'); }); } } async function ensureFontDecrypted(item, tries) { tries = tries || 8; for (let i = 0; i < tries; i++) { try { decryptCxFont(); } catch (e) {} const hasSecret = item && item.querySelector && item.querySelector('.font-cxsecret'); if (!hasSecret) return true; await sleep(500); } return false; } function initFontDecrypt() { const host = location.host; if (host.indexOf('chaoxing') < 0 && host.indexOf('xuexitong') < 0) return; const run = () => { try { decryptCxFont(); } catch (e) {} }; run(); let t = null; const bindObserver = (doc) => { if (!doc || doc._llFontObs) return; doc._llFontObs = true; try { const mo = new MutationObserver(() => { if (t) clearTimeout(t); t = setTimeout(run, 400); }); mo.observe(doc.documentElement, { childList: true, subtree: true }); } catch (e) {} }; collectAccessibleDocs().forEach(bindObserver); setInterval(run, 3000); } function _clampRate(r) { r = Number(r) || 1; if (r < 1) r = 1; if (r > 16) r = 16; return r; } function _clampVolume(v) { v = Number(v); if (isNaN(v)) v = 0; if (v < 0) v = 0; if (v > 1) v = 1; return v; } /** 学习通视频常在「附件 iframe → ananas iframe」嵌套里,需逐层查找 */ function cxTryFrameDoc(frame) { if (!frame) return null; try { return frame.contentDocument || (frame.contentWindow && frame.contentWindow.document) || null; } catch (e) { return null; } } function cxGetVideoFrameDoc(iframeDom) { if (!iframeDom) return null; let doc = cxTryFrameDoc(iframeDom); if (!doc) return null; for (let depth = 0; depth < 5; depth++) { if (doc.querySelector('video, audio, .vjs-big-play-button, #video, .video-js')) return doc; const inner = doc.querySelector('iframe'); if (!inner) break; const innerDoc = cxTryFrameDoc(inner); if (!innerDoc) break; doc = innerDoc; } return doc; } function cxIsVideoLoading(doc) { if (!doc) return false; const loading = doc.querySelector('#loading'); if (!loading) return false; try { const win = doc.defaultView || window; const s = win.getComputedStyle(loading); return s.visibility !== 'hidden' && s.display !== 'none' && Number(s.opacity) !== 0; } catch (e) { return loading.style && loading.style.visibility !== 'hidden'; } } function cxClickVideoPlayButton(doc) { if (!doc || cxIsVideoLoading(doc)) return false; const selectors = [ '.vjs-big-play-button', '#video > button', '.video-js .vjs-play-control', '.playbtn', '.ans-btnplay', 'button[title="播放"]', '.xgplayer-start', '.dplayer-play-icon', '.prism-big-play-btn', '.mvp_btn_play' ]; for (const sel of selectors) { const btn = doc.querySelector(sel); if (!btn) continue; fireClick(btn); try { btn.click(); } catch (e) {} tryJqClick(btn); return true; } const media = doc.querySelector('video, audio'); if (media) { fireClick(media); try { media.click(); } catch (e) {} return true; } return false; } function cxApplyMediaSettings(media) { if (!media) return false; const doc = media.ownerDocument; const rate = _clampRate(SettingsManager.get('videoRate')); const vol = _clampVolume(SettingsManager.get('videoVolume')); let playing = false; try { if (vol <= 0.01) media.muted = true; else { media.muted = false; media.volume = vol; } if (Math.abs(media.playbackRate - rate) > 0.01) media.playbackRate = rate; if (media.paused && !media.ended) { cxClickVideoPlayButton(doc); if (media.readyState < 2) { try { media.load(); } catch (e) {} } const p = media.play(); if (p && typeof p.then === 'function') { p.then(() => { playing = true; }).catch(() => { media.muted = true; cxClickVideoPlayButton(doc); media.play().catch(() => {}); }); } } playing = playing || !media.paused; if (!playing && media.paused) { media.muted = true; cxClickVideoPlayButton(doc); const p2 = media.play(); if (p2 && typeof p2.catch === 'function') p2.catch(() => {}); playing = !media.paused; } } catch (e) {} return playing || !media.paused; } /** 学习通视频内弹题(对齐 OCS videoQuizStrategy) */ function cxHandleVideoQuiz(doc) { const strategy = SettingsManager.get('videoQuizStrategy') || 'random'; if (strategy === 'ignore' || !doc) return false; const opts = doc.querySelectorAll('.ans-videoquiz-opt, .ans-videoquiz-answer, .ans-videoquiz li'); const submit = doc.querySelector('#videoquiz-submit, .ans-videoquiz-submit, .ans-videoquiz .submit'); if (!opts.length && !submit) return false; if (strategy === 'random') { if (opts.length) { const pick = opts.item(Math.floor(Math.random() * opts.length)); if (pick) { fireClick(pick); try { pick.click(); } catch (e) {} } } if (submit) { fireClick(submit); try { submit.click(); } catch (e) {} } const close = doc.querySelector('.ans-videoquiz-close, .layui-layer-close, .ans-videoquiz .close'); if (close) { fireClick(close); try { close.click(); } catch (e) {} } cxLog('视频弹题:已随机作答'); return true; } return false; } function hasZhsCaptcha() { return collectAccessibleDocs().some(d => d.querySelector('.yidun_popup')); } function hasFaceRecognition() { const faceSelectors = [ '.faceRecognition', '#faceRecognition', '.fcqr-box', '.popFace', '.face-pop', '#popFace', '.faceCheck', '.face-check', '.face_verify', '.faceVerify' ]; const faceText = /人脸识别|刷脸验证|人脸采集|身份核验|人脸检测|请进行人脸/; for (const doc of collectAccessibleDocs()) { for (const sel of faceSelectors) { const els = doc.querySelectorAll(sel); for (const el of els) { if (isVisibleEl(el)) return true; } } const dialogs = doc.querySelectorAll('.layui-layer, .el-dialog, .el-message-box, .modal, .ant-modal'); for (const dlg of dialogs) { if (!isVisibleEl(dlg)) continue; if (faceText.test((dlg.innerText || '').trim())) return true; } } return false; } let _safetyNotified = { face: false, captcha: false }; let _pausedForFace = false; function checkSafetyPauses() { let blocked = false; if (SettingsManager.get('faceRecognitionPause') && hasFaceRecognition()) { blocked = true; if (!_safetyNotified.face) { _safetyNotified.face = true; _pausedForFace = true; setPaused(true); panel.tip('检测到人脸识别,请完成后点击「继续答题」'); } } else { if (_safetyNotified.face) { _safetyNotified.face = false; if (_pausedForFace) { _pausedForFace = false; setPaused(false); panel.tip('人脸识别已结束,继续答题'); } } } if (hasZhsCaptcha()) { blocked = true; if (!_safetyNotified.captcha) { _safetyNotified.captcha = true; setPaused(true); panel.tip('智慧树:检测到验证码,请手动完成后点击「继续答题」'); } } else { _safetyNotified.captcha = false; } return blocked; } function initCxSafetyMonitor() { const host = location.host; if (host.indexOf('chaoxing') < 0 && host.indexOf('xuexitong') < 0) return; setInterval(() => { if (!SettingsManager.get('faceRecognitionPause')) return; if (!/\/mycourse\/studentstudy|\/knowledge\/cards/.test(location.href)) return; if (hasFaceRecognition()) { cxCleanup(); checkSafetyPauses(); } }, 2500); } /** 未搜到答案时随机选/填(对齐 OCS randomWork-choice / randomWork-complete) */ async function tryRandomWork(data) { if (SettingsManager.get('reviewMode')) return null; const type = normalizeType(data.type); if (isChoiceType(type) && SettingsManager.get('randomWorkChoice')) { const scene = data.cxScene || getCxQuizScene(); const fresh = cxCollectOptionsForScene(data.$item, scene); const nodes = (fresh && fresh.optionNodes.length) ? fresh.optionNodes : (data.optionNodes || []); if (nodes.length) { const idx = Math.floor(Math.random() * nodes.length); const verified = await fillCxChoice([idx], nodes, type, data.$item, scene); if (verified) { return { ok: true, display: '随机选 ' + String.fromCharCode(65 + idx) }; } } } if ((type === 2 || type === 4 || type === 5 || type === 6) && SettingsManager.get('randomWorkComplete')) { const raw = SettingsManager.get('randomCompleteTexts') || '不会,不知道,待补充'; const texts = String(raw).split(/[,,\n]/).map(s => s.trim()).filter(Boolean); const text = texts[Math.floor(Math.random() * texts.length)] || '不会'; let filled = false; if (data.inputs && data.inputs.length) filled = await fillBlank([text], data.inputs); if (!filled) filled = await fillSubjectiveAnswer([text], data.$item, type); if (filled) return { ok: true, display: '随机填 ' + text }; } return null; } function cxFindChapterQuizIframe() { const cts = $$('.wrap .ans-cc .ans-attach-ct'); for (let i = 0; i < cts.length; i++) { const iframe = cts[i].querySelector('iframe'); if (!iframe) continue; try { const src = iframe.src || iframe.getAttribute('src') || ''; const idoc = iframe.contentDocument || iframe.contentWindow.document; if (idoc && (idoc.querySelector('.TiMu, .clearfix .TiMu') || /work|Work|chapter/i.test(src))) { return iframe; } } catch (e) {} } return null; } /** 章节测验是否已完成(对齐 OCS testTit_status_complete / 页面任务点已完成) */ function cxIsChapterQuizDone(iframeDom) { for (const doc of collectAccessibleDocs()) { if (doc.querySelector('.ans-attach-ct.ans-job-finished, .ans-job-finished')) return true; const cc = doc.querySelector('.wrap .ans-cc, .ans-cc'); if (cc && /任务点已完成/.test(cc.innerText || '')) return true; if (/\/knowledge\/cards/.test(location.href) && /任务点已完成/.test(doc.body && doc.body.innerText || '')) { return true; } } if (iframeDom) { try { const win = iframeDom.contentWindow; const idoc = iframeDom.contentDocument || (win && win.document); if (idoc) { const status = idoc.querySelector('.testTit_status, .newTestTitle .testTit_status'); if (status && (status.classList.contains('testTit_status_complete') || /已完成|已提交/.test((status.textContent || '').trim()))) { return true; } if (idoc.querySelector('.testTit_status_complete, .saveWorkDone, .SubmitSucc, .workDone, .submitSucc')) { return true; } } if (typeof isChapterQuizAlreadySubmitted === 'function' && isChapterQuizAlreadySubmitted(win, idoc)) { return true; } } catch (e) {} } const raw = cxParseParams(); if (raw && raw !== '$mArg') { try { const data = JSON.parse(raw); const works = (data.attachments || []).filter(t => t.type === 'workid' || (t.property && t.property.module === 'workid')); if (works.length && works.every(cxIsPassed)) return true; } catch (e) {} } return false; } function cxNeedsChapterQuizWork() { const iframe = cxFindChapterQuizIframe(); if (cxIsChapterQuizDone(iframe)) return false; if (cxHasPendingWorkQuiz()) return true; if (SettingsManager.get('workWhenNoJob') && hasChapterQuizDoc()) return true; return false; } /** 智慧树答完后逐题翻页暂存(对齐 OCS gxkWorkAndExam) */ async function zhsPersistAnswers() { const doc = document; const cards = doc.querySelectorAll('.answerCard_list li, .answerCard_list1 li, .answerCard ul li, .answerCard li'); const subjects = doc.querySelectorAll('.examPaper_subject, .questionBox, .ques-detail'); const total = cards.length || subjects.length; if (!total) return; panel.tip('智慧树:正在逐题暂存(' + total + '题)…'); for (let i = 0; i < total; i++) { if (cards.length && cards[i]) { fireClick(cards[i]); try { cards[i].click(); } catch (e) {} await sleep(500); } else if (subjects[i]) { try { subjects[i].scrollIntoView({ block: 'center', behavior: 'auto' }); } catch (e) {} await sleep(300); } const next = doc.querySelector('.Topicswitchingbtn:not(.Topicswitchingbtn-gray)') || doc.querySelector('.next-topic:not(.noNext)') || Array.from(doc.querySelectorAll('.el-button, button, a, .Topicswitchingbtn')) .find(b => /下一题/.test((b.textContent || b.value || '').trim()) && !b.classList.contains('is-disabled') && !b.classList.contains('Topicswitchingbtn-gray')); if (next) { fireClick(next); try { next.click(); } catch (e) {} await sleep(1500); } } } // ========== 智慧树 视频/音频前台自动播放 ========== function initMediaAuto() { const host = location.host; const isZhs = host.indexOf('zhihuishu') >= 0 || host.indexOf('zjooc') >= 0 || host.indexOf('hiknow') >= 0; if (!isZhs) return; let captchaTip = false; setInterval(() => { const hv = SettingsManager.get('handleVideo'); const ha = SettingsManager.get('handleAudio'); if (!hv && !ha) return; if (collectAccessibleDocs().some(d => d.querySelector('.yidun_popup'))) { if (!captchaTip) { captchaTip = true; setPaused(true); panel.tip('智慧树:检测到验证码,请手动完成后点击「继续答题」'); } return; } captchaTip = false; collectAccessibleDocs().forEach(doc => { if (hv) { Array.from(doc.querySelectorAll('video')).forEach(v => { try { const bar = doc.querySelector('.controlsBar, .vjs-control-bar'); if (bar) { bar.style.display = 'block'; bar.style.opacity = '1'; } cxApplyMediaSettings(v); } catch (e) {} }); } if (ha) { Array.from(doc.querySelectorAll('audio')).forEach(a => { try { cxApplyMediaSettings(a); } catch (e) {} }); } }); }, 2500); } // ========== 学习通 视频/音频 前台播放(对齐 OCS JobRunner.media) ========== const CX = { queue: [], frames: [], attachIndices: [], defaults: null, processing: false, jumping: false, quizWaiting: false, videoInterval: null, quizInterval: null, errorInterval: null, videoActive: false, videoWorker: null, videoTaskCounter: 0, currentVideoTaskId: null, _iframeWait: 0, studyRunning: false, studyMode: false, searchedMids: null, _jobDoneResolve: null, completedMids: null, _currentMid: null }; let _cxProgressLastPct = -1; let _cxProgressLastTs = 0; function signalWorkQuizDone() { const mark = (win) => { if (!win) return; try { win.__bookwk_work_done = true; } catch (e) {} }; try { mark(unsafeWindow); } catch (e) {} try { if (typeof window !== 'undefined') mark(window); } catch (e) {} try { if (unsafeWindow.parent && unsafeWindow.parent !== unsafeWindow) mark(unsafeWindow.parent); } catch (e) {} try { if (unsafeWindow.top) mark(unsafeWindow.top); } catch (e) {} } function clearWorkQuizDoneFlag() { const clear = (win) => { if (!win) return; try { win.__bookwk_work_done = false; } catch (e) {} }; try { clear(unsafeWindow); } catch (e) {} try { if (typeof window !== 'undefined') clear(window); } catch (e) {} try { if (unsafeWindow.top) clear(unsafeWindow.top); } catch (e) {} } function cxHasPendingWorkQuiz() { const iframe = cxFindChapterQuizIframe(); if (cxIsChapterQuizDone(iframe)) return false; const raw = cxParseParams(); if (raw && raw !== '$mArg') { try { const data = JSON.parse(raw); const list = data.attachments || []; if (list.some(t => (t.type === 'workid' || (t.property && t.property.module === 'workid')) && !cxIsPassed(t))) { return true; } } catch (e) {} } if (/待完成/.test(document.body && document.body.innerText || '')) { if (document.querySelector('.ans-attach-ct iframe[src*="work"], .ans-attach-ct iframe[src*="Work"]')) return true; } return false; } function cxLog(msg) { try { panel.tip(msg); } catch (e) {} } function getCookie(name) { return ('; ' + document.cookie).split('; ' + name + '=').pop().split(';')[0]; } function cxIsPassed(task) { if (!task) return false; if (task.isPassed === true) return true; if (task.status === 'completed' || task.status === 'finished') return true; if (task.finished === true) return true; return false; } function cxFormatDuration(seconds) { if (!seconds || seconds < 0) return '00:00'; const total = Math.max(0, Math.floor(Number(seconds) || 0)); const h = Math.floor(total / 3600); const m = Math.floor((total % 3600) / 60); const s = total % 60; const pad = (n) => String(n).padStart(2, '0'); if (h > 0) return pad(h) + ':' + pad(m) + ':' + pad(s); return pad(m) + ':' + pad(s); } function cxGenerateEnc(classId, uid, jobId, objectId, playTime, duration) { const md5fn = _getMd5(); if (!md5fn) return ''; const str = '[' + classId + '][' + uid + '][' + jobId + '][' + objectId + '][' + (playTime * 1000) + '][d_yHJ!$pdA~5][' + (duration * 1000) + '][0_' + duration + ']'; return md5fn(str); } function cxUpdateSimulateProgress(playTime, duration, name) { if (!duration) return; const pct = Math.min(100, Math.round((playTime / duration) * 100)); const now = Date.now(); if (pct === _cxProgressLastPct && now - _cxProgressLastTs < 5000) return; if (pct - _cxProgressLastPct < 2 && now - _cxProgressLastTs < 3000 && pct < 100) return; _cxProgressLastPct = pct; _cxProgressLastTs = now; const remain = Math.max(0, duration - playTime); cxLog('模拟上报 ' + String(name || '').slice(0, 10) + ' ' + pct + '% ' + cxFormatDuration(playTime) + '/' + cxFormatDuration(duration) + ' 余' + cxFormatDuration(remain)); } function cxVerifyPassedFallback(ctx, worker) { if (worker.aborted) return; const statusUrl = location.protocol + '//' + location.host + '/ananas/status/' + ctx.objectId + '?k=' + (getCookie('fid') || '') + '&_uid=' + ctx.uid + '&flag=normal&_dc=' + Date.now(); GM_xmlhttpRequest({ method: 'GET', url: statusUrl, headers: { Host: location.host, Referer: location.protocol + '//' + location.host + '/ananas/modules/video/index.html' }, onload(res) { if (worker.aborted) return; try { const info = JSON.parse(res.responseText); if (info.isPassed) { cxLog('兜底确认任务已通过:' + ctx.name); if (!worker.isCompleted) { worker.isCompleted = true; if (worker.completeCallback) worker.completeCallback(); } } else if (worker.completeCallback) { cxLog('兜底查询仍未通过,继续下一任务'); worker.completeCallback(); } } catch (e) { if (worker.completeCallback) worker.completeCallback(); } }, onerror() { if (!worker.aborted && worker.completeCallback) worker.completeCallback(); } }); } function cxCreateReportWorker(videoInfo, taskObj) { const name = videoInfo.name; const duration = videoInfo.duration; const playedTime = videoInfo.playedTime || 0; const dtoken = videoInfo.dtoken; const objectId = videoInfo.objectId; const jobId = taskObj.jobid; const rt = videoInfo.rt; const otherInfo = videoInfo.otherInfo; const d = CX.defaults || {}; const reportUrl = d.reportUrl || (location.protocol + '//' + location.host + '/multimedia/report/data'); const classId = d.clazzId || d.clazzid || ''; const uid = getCookie('_uid') || getCookie('UID'); const rate = _clampRate(SettingsManager.get('videoRate')); const reportInterval = Number(SettingsManager.get('reportInterval')) || 50; const workerCode = ` var timer = null; var currentTime = 0; var duration = ${duration}; var rate = ${rate}; var playedTime = ${playedTime}; var reportInterval = ${reportInterval}; var lastReportTime = 0; var isRunning = true; var startTime = Date.now(); function sendReport(playTime, isComplete, isFirst) { self.postMessage({ type: 'doReport', playTime: playTime, isComplete: isComplete, isFirst: isFirst || false }); } sendReport(0, false, true); var fastRate = 10; function updateLoop() { if (!isRunning) return; var now = Date.now(); var effectiveRate = (currentTime < playedTime) ? fastRate : rate; var targetTime = Math.min(currentTime + (effectiveRate * 0.2), duration); var isCatchingUp = false; if (targetTime > currentTime) { if (targetTime - currentTime > reportInterval * 1.5) { isCatchingUp = true; currentTime = Math.min(currentTime + reportInterval, targetTime); } else { currentTime = targetTime; } var timeSinceLastReport = currentTime - lastReportTime; var shouldReport = timeSinceLastReport >= reportInterval || currentTime >= duration; if (shouldReport) { var reportTime = currentTime >= duration ? duration : Math.ceil(currentTime); sendReport(reportTime, currentTime >= duration); lastReportTime = currentTime; } self.postMessage({ type: 'progress', currentTime: currentTime, duration: duration }); } if (currentTime >= duration) { isRunning = false; if (timer) clearInterval(timer); self.postMessage({ type: 'completed' }); } else { var nextDelay = isCatchingUp ? 3000 : 200; timer = setTimeout(updateLoop, nextDelay); } } updateLoop(); self.onmessage = function(e) { var data = e.data; if (data.type === 'updateRate') rate = data.rate; else if (data.type === 'stop') { isRunning = false; if (timer) clearTimeout(timer); self.postMessage({ type: 'stopped' }); } }; `; const blob = new Blob([workerCode], { type: 'application/javascript' }); const worker = new Worker(URL.createObjectURL(blob)); worker.videoContext = { name, duration, dtoken, objectId, jobId, rt, otherInfo, reportUrl, classId, uid }; worker.onmessage = function(e) { const data = e.data; switch (data.type) { case 'progress': if (worker.aborted) break; cxUpdateSimulateProgress(data.currentTime, data.duration, worker.videoContext.name); break; case 'doReport': { if (worker.aborted) break; const ctx = worker.videoContext; let isdrag; if (data.isFirst) isdrag = '3'; else if (data.isComplete) isdrag = '4'; else isdrag = '0'; const doReportRequest = (retryCount) => { if (worker.aborted) return; retryCount = retryCount || 0; const maxRetries = 10; const enc = cxGenerateEnc(ctx.classId, ctx.uid, ctx.jobId, ctx.objectId, data.playTime, ctx.duration); const reportsUrl = ctx.reportUrl + '/' + ctx.dtoken + '?clazzId=' + ctx.classId + '&playingTime=' + data.playTime + '&duration=' + ctx.duration + '&clipTime=0_' + ctx.duration + '&objectId=' + ctx.objectId + '&otherInfo=' + ctx.otherInfo + '&jobid=' + ctx.jobId + '&userid=' + ctx.uid + '&isdrag=' + isdrag + '&view=pc' + '&enc=' + enc + '&rt=' + ctx.rt + '&dtype=Video' + '&_t=' + Date.now(); GM_xmlhttpRequest({ method: 'GET', url: reportsUrl, headers: { Host: location.host, Referer: location.protocol + '//' + location.host + '/ananas/modules/video/index.html', 'Content-Type': 'application/json', 'Sec-Fetch-Site': 'same-origin' }, onload(res) { if (worker.aborted) return; try { const result = JSON.parse(res.responseText); if (result.isPassed) { if (!worker.isCompleted) { worker.isCompleted = true; cxLog('视频任务已完成:' + ctx.name); if (worker.completeCallback) worker.completeCallback(); } } else if (isdrag === '4' && retryCount < maxRetries) { setTimeout(() => doReportRequest(retryCount + 1), 3000); } else if (isdrag === '4') { cxVerifyPassedFallback(ctx, worker); } } catch (e) { if (isdrag === '4' && retryCount < maxRetries) { setTimeout(() => doReportRequest(retryCount + 1), 3000); } } }, onerror() { if (worker.aborted) return; if (isdrag === '4' && retryCount < maxRetries) { setTimeout(() => doReportRequest(retryCount + 1), 3000); } } }); }; doReportRequest(0); break; } case 'completed': if (worker.aborted) break; setTimeout(() => { if (worker.aborted) return; if (!worker.isCompleted && worker.completeCallback) { cxLog('等待服务器确认超时,继续下一任务'); worker.completeCallback(); } }, 35000); break; } }; return worker; } function cxGetVideoStatus(item, callback, iframeDom) { const objectId = item.property.objectid; const uid = getCookie('_uid') || getCookie('UID') || ''; const statusUrl = location.protocol + '//' + location.host + '/ananas/status/' + objectId + '?k=' + (getCookie('fid') || '') + '&_uid=' + uid + '&flag=normal&_dc=' + Date.now(); let taskPlayedTime = 0; if (item.headOffset && item.headOffset > 0) { taskPlayedTime = Math.floor(item.headOffset / 1000); } else if (item.playTime && item.playTime > 0) { taskPlayedTime = Math.floor(item.playTime / 1000); } let iframePlayedTime = 0; if (iframeDom) { try { const doc = cxGetVideoFrameDoc(iframeDom) || iframeDom.contentDocument; const videoEl = doc && doc.querySelector('video'); if (videoEl && videoEl.currentTime > 0) { iframePlayedTime = Math.floor(videoEl.currentTime); } } catch (e) {} } GM_xmlhttpRequest({ method: 'GET', url: statusUrl, headers: { Host: location.host, Referer: location.protocol + '//' + location.host + '/ananas/modules/video/index.html' }, onload(res) { try { const videoInfo = JSON.parse(res.responseText); const playedTime = Math.max(taskPlayedTime, iframePlayedTime); callback({ success: true, duration: videoInfo.duration, dtoken: videoInfo.dtoken, objectId: objectId, rt: (item.property && item.property.rt) || '0.9', otherInfo: item.otherInfo || '', jobid: item.jobid, playedTime: playedTime, isPassed: videoInfo.isPassed || false, name: item.property.name }); } catch (e) { callback({ success: false, error: e.message }); } }, onerror(err) { callback({ success: false, error: (err && err.error) || 'network' }); } }); } function cxSimulateVideoReport(videoInfo, taskObj, iframeDom) { const name = videoInfo.name || (taskObj.property && taskObj.property.name) || '视频'; const duration = videoInfo.duration; if (videoInfo.isPassed) { cxLog('视频已完成,跳过:' + name); cxComplete(); return; } const playedTime = videoInfo.playedTime || 0; if (playedTime >= duration && duration > 0) { cxLog('本地已播完但服务器未认定,重新完整上报:' + name); } else if (playedTime > 0 && playedTime < duration) { cxLog('模拟上报:' + name + ',已看' + cxFormatDuration(playedTime) + '将10x跳过'); } else { cxLog('模拟上报:' + name + ',时长 ' + cxFormatDuration(duration)); } const taskId = ++CX.videoTaskCounter; CX.currentVideoTaskId = taskId; _cxProgressLastPct = -1; _cxProgressLastTs = 0; CX.videoActive = true; cxUpdateSimulateProgress(playedTime, duration, name); const worker = cxCreateReportWorker(videoInfo, taskObj); CX.videoWorker = worker; const workerTaskId = taskId; worker.completeCallback = function() { if (workerTaskId !== CX.currentVideoTaskId || !CX.videoActive) return; try { if (iframeDom) { const doc = cxGetVideoFrameDoc(iframeDom); if (doc) { const mediaEl = doc.querySelector('video') || doc.querySelector('audio'); if (mediaEl) { mediaEl.dispatchEvent(new Event('ended', { bubbles: true })); cxLog('已触发 ended 事件,任务点UI将更新'); } } } } catch (e) {} CX.videoActive = false; cxComplete(); }; } function cxCleanup() { if (CX.videoWorker) { const w = CX.videoWorker; CX.videoWorker = null; try { w.aborted = true; w.postMessage({ type: 'stop' }); setTimeout(() => { try { w.terminate(); } catch (e) {} }, 100); } catch (e) {} } if (CX.videoInterval) { clearInterval(CX.videoInterval); CX.videoInterval = null; } if (CX.quizInterval) { clearInterval(CX.quizInterval); CX.quizInterval = null; } if (CX.errorInterval) { clearInterval(CX.errorInterval); CX.errorInterval = null; } CX.videoActive = false; CX.currentVideoTaskId = null; } /** 模拟上报运行中热更新倍速(对齐 OK currentVideoWorker.postMessage updateRate) */ function cxApplySimulateRate(rate) { const r = _clampRate(rate); if (!CX.videoWorker || CX.videoWorker.aborted || !CX.videoActive) return false; try { CX.videoWorker.postMessage({ type: 'updateRate', rate: r }); return true; } catch (e) { return false; } } /** 前台真实播放:倍速>1 强制回 1(>=2 同样回 1) */ function cxEnforceNormalVideoRate(box) { let rate = Number(SettingsManager.get('videoRate')) || 1; if (rate <= 1) return false; SettingsManager.set('videoRate', 1); const rateInp = box && box.querySelector('[data-key="videoRate"]'); if (rateInp) rateInp.value = 1; panel.tip('前台真实播放仅支持1倍速,已自动调回1'); return true; } function cxSwitchToSimulateAndReload(box, rate) { const v = _clampRate(rate); SettingsManager.set('videoRate', v); SettingsManager.set('videoMode', 'simulate'); const rateInp = box && box.querySelector('[data-key="videoRate"]'); const modeSel = box && box.querySelector('[data-key="videoMode"]'); if (rateInp) rateInp.value = v; if (modeSel) modeSel.value = 'simulate'; panel.tip('倍速超过1,已切换模拟上报,正在刷新页面…'); setTimeout(() => { try { location.reload(); } catch (e) {} }, 800); } function cxSwitchToSimulateModeAndReload(box) { SettingsManager.set('videoMode', 'simulate'); const modeSel = box && box.querySelector('[data-key="videoMode"]'); if (modeSel) modeSel.value = 'simulate'; panel.tip('已切换模拟上报,正在刷新页面…'); setTimeout(() => { try { location.reload(); } catch (e) {} }, 800); } function cxSwitchToNormalAndReload(box) { SettingsManager.set('videoMode', 'normal'); cxEnforceNormalVideoRate(box); const modeSel = box && box.querySelector('[data-key="videoMode"]'); if (modeSel) modeSel.value = 'normal'; panel.tip('已切换前台真实播放,正在刷新页面…'); setTimeout(() => { try { location.reload(); } catch (e) {} }, 800); } function cxWaitForMedia(doc, timeout) { return new Promise((resolve) => { timeout = timeout || 30000; const start = Date.now(); const tick = () => { const media = doc && (doc.querySelector('video') || doc.querySelector('audio')); if (media) return resolve(media); if (Date.now() - start > timeout) return resolve(null); setTimeout(tick, 500); }; tick(); }); } function cxFixedVideoProgress(doc) { if (!doc) return; const bar = doc.querySelector('.vjs-control-bar'); if (bar) { bar.style.opacity = '1'; bar.style.display = 'block'; } } function cxPlayMedia(media) { return new Promise((resolve) => { if (!media) return resolve(false); try { const p = media.play(); if (p && typeof p.then === 'function') { p.then(() => resolve(true)).catch(() => { media.muted = true; const p2 = media.play(); if (p2 && typeof p2.then === 'function') { p2.then(() => resolve(true)).catch(() => resolve(false)); } else resolve(false); }); } else resolve(true); } catch (e) { resolve(false); } }); } function cxStartVideoQuizLoop(doc) { if (CX.quizInterval) clearInterval(CX.quizInterval); const strategy = SettingsManager.get('videoQuizStrategy') || 'random'; if (strategy === 'ignore' || !doc) return; const tick = async () => { if (!CX.videoActive) return; const submitBtn = doc.querySelector('#videoquiz-submit'); if (!submitBtn) return; const list = Array.from(doc.querySelectorAll('.ans-videoquiz-opt label')); if (list.length) { const pick = list[Math.floor(Math.random() * list.length)]; if (pick) { fireClick(pick); try { pick.click(); } catch (e) {} } } fireClick(submitBtn); try { submitBtn.click(); } catch (e) {} await sleep(3000); const container = doc.querySelector('#video .ans-videoquiz'); if (container) container.remove(); doc.querySelectorAll('.x-component-default').forEach(com => { com.style.display = 'none'; }); cxLog('视频弹题:已随机作答'); }; CX.quizInterval = setInterval(() => { tick().catch(() => {}); }, 3000); } function cxGetAttachments() { const raw = cxParseParams(); if (!raw || raw === '$mArg') return []; try { const data = JSON.parse(raw); if (data && data.defaults) CX.defaults = data.defaults; return data.attachments || []; } catch (e) { return []; } } function cxAttMid(att, index) { if (!att) return ''; return (att.property && att.property.mid) || att.jobid || ('idx_' + (index != null ? index : '')); } function cxIsTaskDone(att, index) { if (!att) return true; if (cxIsPassed(att)) return true; if (CX.completedMids && CX.completedMids.has(cxAttMid(att, index))) return true; return false; } function cxCountPendingAttachments() { const all = cxGetAttachments(); return all.filter((t, i) => !cxIsTaskDone(t, i)).length; } async function cxWaitAttachIframe(timeout) { timeout = timeout || 30000; const start = Date.now(); while (Date.now() - start < timeout) { const iframe = cxResolveTaskIframe(); if (iframe) { try { const src = iframe.src || iframe.getAttribute('src') || ''; if (!src || src === 'about:blank') { await sleep(500); continue; } } catch (e) {} return iframe; } await sleep(500); } return null; } function cxGetStudyScope() { return document.querySelector('.wrap .ans-cc') || document.querySelector('.ans-cc') || document; } function cxQueryInScope(sel) { const scope = cxGetStudyScope(); try { if (scope === document) return $$(sel); return Array.from(scope.querySelectorAll(sel.replace(/^\.ans-cc\s+/, ''))); } catch (e) { return $$(sel); } } function cxClickUnpassedTab() { const rounds = cxQueryInScope('.roundpointStudent, .roundpoint'); for (const r of rounds) { const ct = r.closest('.ans-attach-ct'); if (ct && ct.classList.contains('ans-job-finished')) continue; const li = r.closest('li') || r.parentElement; if (!li) continue; if (li.closest && li.closest('.ans-job-finished')) continue; const clickEl = li.querySelector('span, a') || li; fireClick(clickEl); try { clickEl.click(); } catch (e) {} return true; } return false; } function cxClickAttachTabByIndex(index) { const rounds = cxQueryInScope('.roundpointStudent, .roundpoint'); if (rounds[index]) { const clickEl = rounds[index].closest('li') || rounds[index].parentElement || rounds[index]; fireClick(clickEl); try { clickEl.click(); } catch (e) {} return true; } const tabs = cxGetAttachTabItems(); if (tabs[index]) { fireClick(tabs[index]); try { tabs[index].click(); } catch (e) {} return true; } return false; } function cxClickTaskByName(name) { if (!name) return false; const full = String(name).trim(); const short = full.replace(/^\d+[.、\s]*/, '').trim().slice(0, 10); const scope = cxGetStudyScope(); const candidates = scope.querySelectorAll( 'a, li, .hover, span, .title, h1, h2, h3, .headtitle, .jobName, .jobtitle, .catalog_title'); for (const el of candidates) { const text = (el.textContent || '').trim(); if (!text || text.length < 2) continue; if (text === full || text.indexOf(full) >= 0 || (short.length >= 2 && text.indexOf(short) >= 0)) { fireClick(el); try { el.click(); } catch (e) {} return true; } } return false; } function cxRunStudyTask(type, iframe, att) { if (type === 'video') cxProcessVideo(iframe, att); else if (type === 'audio') cxProcessAudio(iframe, att); else if (type === 'document') cxProcessDoc(att, 'document'); else if (type === 'read') cxProcessDoc(att, 'read'); else if (type === 'insertbook') cxProcessDoc(att, 'book'); else if (type === 'workid') cxProcessWork(iframe, att); else { cxLog('跳过任务类型: ' + type); cxComplete(); } } async function cxExecuteStudyTask(type, iframe, att, mid, name) { CX.processing = true; CX._currentMid = mid; cxLog('处理任务:' + name); await new Promise((resolve) => { let settled = false; const finish = () => { if (settled) return; settled = true; resolve(); }; CX._jobDoneResolve = finish; const timer = setTimeout(() => { if (!settled) { cxLog('任务等待超时,继续下一项:' + name); CX._jobDoneResolve = null; CX.processing = false; finish(); } }, 60 * 60 * 1000); const origFinish = finish; CX._jobDoneResolve = () => { clearTimeout(timer); origFinish(); }; cxRunStudyTask(type, iframe, att); if (!CX._jobDoneResolve) clearTimeout(timer); }); CX.processing = false; CX._currentMid = null; } function cxSearchAllIframes() { const result = []; const seen = new Set(); const walk = (doc) => { if (!doc || seen.has(doc)) return; seen.add(doc); doc.querySelectorAll('iframe').forEach(frame => { if (!frame || seen.has(frame)) return; seen.add(frame); result.push(frame); try { const fd = frame.contentDocument || (frame.contentWindow && frame.contentWindow.document); if (fd) walk(fd); } catch (e) {} }); }; walk(document); return result; } function cxParseFrameJobId(iframe) { try { let dataStr = iframe.getAttribute('data'); if (!dataStr) { const win = iframe.contentWindow; const fe = win && win.frameElement; if (fe) dataStr = fe.getAttribute('data'); try { const pfe = win && win.parent && win.parent.frameElement; if (!dataStr && pfe) dataStr = pfe.getAttribute('data'); } catch (e) {} } const data = JSON.parse(dataStr || '{}'); return data.jobid || data._jobid || null; } catch (e) { return null; } } /** 对齐 OCS searchJob:从当前页面 iframe 匹配 jobid 找待处理任务(辅助快速路径) */ function cxSearchNextJob() { const attachments = cxGetAttachments(); if (!attachments.length) return null; for (const iframe of cxSearchAllIframes()) { try { const idoc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document); if (!idoc) continue; const jobid = cxParseFrameJobId(iframe); if (!jobid) continue; const attIndex = attachments.findIndex(a => String(a.jobid || '') === String(jobid)); if (attIndex < 0) continue; const att = attachments[attIndex]; if (cxIsTaskDone(att, attIndex)) continue; const type = att.type || (att.property && att.property.module); const name = (att.property && (att.property.name || att.property.title)) || '任务'; const mid = cxAttMid(att, attIndex); const hasVideo = idoc.querySelector('#video, #audio, video, audio'); const hasQuiz = idoc.querySelector('.TiMu'); const hasRead = idoc.querySelector('#img.imglook, .swiper-container, #hyperlink'); if ((type === 'video' || type === 'audio') && !hasVideo) continue; if (type === 'workid' && !hasQuiz) continue; if ((type === 'read' || type === 'document' || type === 'insertbook') && !hasRead && !hasVideo) continue; return { name, attachment: att, iframe, type, mid }; } catch (e) {} } return null; } async function cxRunStudyLoop() { if (CX.studyRunning) return; CX.studyRunning = true; CX.studyMode = true; CX.searchedMids = new Set(); if (!CX.completedMids) CX.completedMids = new Set(); const all = cxGetAttachments(); const getPending = () => all.map((t, i) => ({ t, i })).filter(x => !cxIsTaskDone(x.t, x.i)); let pendingList = getPending(); if (!pendingList.length) { CX.studyMode = false; CX.studyRunning = false; return; } cxLog('发现 ' + pendingList.length + ' 个待完成任务'); const quickJob = cxSearchNextJob(); if (quickJob) { await cxExecuteStudyTask(quickJob.type, quickJob.iframe, quickJob.attachment, quickJob.mid, quickJob.name); const left = cxCountPendingAttachments(); if (left > 0) cxLog('剩余 ' + left + ' 个任务点…'); await sleep(1200); } while (true) { pendingList = getPending(); if (!pendingList.length) break; const beforeCount = pendingList.length; let processedAny = false; for (const { t: att, i: index } of pendingList) { const type = att.type || (att.property && att.property.module); const name = (att.property && (att.property.name || att.property.title)) || ('任务' + (index + 1)); const mid = cxAttMid(att, index); const needsIframe = ['video', 'audio', 'workid'].indexOf(type) >= 0; cxClickAttachTabByIndex(index); cxClickTaskByName(name); let iframe = cxResolveIframeByIndex(index); let iframeSrc = ''; if (iframe) { try { iframeSrc = iframe.src || iframe.getAttribute('src') || ''; } catch (e) {} } if (!iframe || !iframeSrc || iframeSrc === 'about:blank') { iframe = await cxSwitchToAttachTab(index, att); } if (!iframe) iframe = await cxWaitAttachIframe(20000); if (needsIframe && !iframe) { cxLog('iframe未就绪,稍后重试:' + name); continue; } await cxExecuteStudyTask(type, iframe, att, mid, name); processedAny = true; const left = cxCountPendingAttachments(); if (left > 0) cxLog('剩余 ' + left + ' 个任务点…'); await sleep(1200); } pendingList = getPending(); if (!pendingList.length) break; if (!processedAny || pendingList.length >= beforeCount) { const left = cxCountPendingAttachments(); if (left > 0) cxLog('剩余 ' + left + ' 个任务点…'); break; } } CX.studyMode = false; CX.studyRunning = false; CX._jobDoneResolve = null; } function cxResolveTaskIframe(obj) { if (CX.frames && CX.frames[0]) return CX.frames[0]; const ct = $$('.wrap .ans-cc .ans-attach-ct')[0]; return ct ? ct.querySelector('iframe') : null; } function cxResolveIframeByIndex(index) { const containers = $$('.wrap .ans-cc .ans-attach-ct'); if (containers.length > 1 && index >= 0 && index < containers.length) { const iframe = containers[index].querySelector('iframe'); if (iframe) return iframe; } return cxResolveTaskIframe(); } /** 获取章节内多个任务点的 tab 列表(仅限 ans-cc 区域,避免误点右侧章节目录) */ function cxGetAttachTabItems() { const scope = cxGetStudyScope(); const round = scope.querySelectorAll('.roundpointStudent, .roundpoint'); if (round.length > 1) { const parents = Array.from(round).map(r => r.closest('li') || r.parentElement).filter(Boolean); if (parents.length > 1) return parents; } const selectors = [ 'ul.ans-tab li', 'ul.clearfix li', '#cardFocus ul li', '.stu-nav-tabs li', '.joblist li', '.catalog_points li' ]; for (const sel of selectors) { const items = Array.from(scope.querySelectorAll(sel)); if (items.length > 1) return items; } return []; } /** 切换到第 index 个任务点 tab 并等待 iframe 就绪 */ async function cxSwitchToAttachTab(index, task) { const name = task && task.property && (task.property.name || task.property.title); if (name) cxClickTaskByName(String(name).trim()); cxClickAttachTabByIndex(index); cxClickUnpassedTab(); await sleep(2000); const tabs = cxGetAttachTabItems(); if (tabs.length && index >= 0 && index < tabs.length) { const tab = tabs[index]; const clickEl = tab.querySelector('span, a') || tab; fireClick(clickEl); try { clickEl.click(); } catch (e) {} fireClick(tab); try { tab.click(); } catch (e) {} await sleep(1500); } else if (name) { const scope = cxGetStudyScope(); const candidates = scope.querySelectorAll('li, a, span, .hover'); const full = String(name).trim(); for (const el of candidates) { const text = (el.textContent || '').trim(); if (!text || text.length < 2) continue; if (text.indexOf(full) >= 0 || full.indexOf(text.slice(0, 10)) >= 0) { fireClick(el); try { el.click(); } catch (e) {} await sleep(1500); break; } } } for (let i = 0; i < 30; i++) { const ct = $('.wrap .ans-cc .ans-attach-ct') || cxGetStudyScope().querySelector('.ans-attach-ct'); const iframe = ct && ct.querySelector('iframe'); if (iframe) { const src = iframe.src || iframe.getAttribute('src') || ''; if (!src || src === 'about:blank') { await sleep(500); continue; } return iframe; } await sleep(500); } return null; } async function cxWaitForMediaReady(doc, timeout) { timeout = timeout || 120000; const start = Date.now(); while (Date.now() - start < timeout) { const media = doc && (doc.querySelector('video') || doc.querySelector('audio')); const holder = doc && doc.querySelector('#video, #audio'); if (media && (media.readyState >= 1 || media.src || holder)) return media; await sleep(200); } return null; } function cxNormalPlay(iframeDom, obj) { const name = (obj.property && obj.property.name) || '视频'; iframeDom = iframeDom || cxResolveTaskIframe(obj); if (!iframeDom) { CX._iframeWait = (CX._iframeWait || 0) + 1; if (CX._iframeWait > 20) { CX._iframeWait = 0; cxLog('视频iframe加载超时,跳过:' + name); cxCleanup(); cxComplete(); return; } setTimeout(() => cxNormalPlay(null, obj), 2000); return; } CX._iframeWait = 0; CX.videoActive = true; let rate = _clampRate(SettingsManager.get('videoRate')); if ((SettingsManager.get('videoMode') || 'normal') === 'normal' && rate > 1) rate = 1; const vol = _clampVolume(SettingsManager.get('videoVolume')); let doc; try { doc = cxGetVideoFrameDoc(iframeDom); } catch (e) {} if (!doc) { try { doc = iframeDom.contentDocument; } catch (e) {} } if (!doc) { cxLog('视频文档未就绪,重试:' + name); CX.videoActive = false; setTimeout(() => cxNormalPlay(iframeDom, obj), 2000); return; } cxFixedVideoProgress(doc); cxStartVideoQuizLoop(doc); if (CX.errorInterval) clearInterval(CX.errorInterval); CX.errorInterval = setInterval(() => { const errorDiv = doc.querySelector('.vjs-modal-dialog-content'); if (errorDiv && ['视频文件损坏', '网络错误导致视频下载中途失败', '视频因格式不支持', '网络的问题无法加载'] .some(s => (errorDiv.innerText || '').includes(s))) { cxLog('视频加载失败,跳过:' + name); if (CX.errorInterval) { clearInterval(CX.errorInterval); CX.errorInterval = null; } CX.videoActive = false; if (CX.videoInterval) { clearInterval(CX.videoInterval); CX.videoInterval = null; } cxComplete(); } }, 3000); let executed = false; const waitStart = Date.now(); const MAX_WAIT = 90000; if (CX.videoInterval) clearInterval(CX.videoInterval); CX.videoInterval = setInterval(() => { if (!CX.videoActive) return; if (!executed && Date.now() - waitStart > MAX_WAIT) { cxLog('视频等待超时,跳过:' + name); clearInterval(CX.videoInterval); CX.videoInterval = null; CX.videoActive = false; cxComplete(); return; } if (cxIsVideoLoading(doc)) { cxClickVideoPlayButton(doc); return; } const media = doc.querySelector('video') || doc.querySelector('audio'); if (!media) { cxClickVideoPlayButton(doc); return; } if (!executed) { executed = true; cxLog('开始播放:' + name); const onPause = async () => { if (checkSafetyPauses() || media.ended) return; await sleep(800); media.playbackRate = rate; await cxPlayMedia(media); }; const onEnded = () => { media.removeEventListener('pause', onPause); media.removeEventListener('ended', onEnded); cxLog('播放完毕:' + name); if (CX.errorInterval) { clearInterval(CX.errorInterval); CX.errorInterval = null; } if (CX.videoInterval) { clearInterval(CX.videoInterval); CX.videoInterval = null; } CX.videoActive = false; cxComplete(); }; media.addEventListener('pause', onPause); media.addEventListener('ended', onEnded); if (media.ended) { onEnded(); return; } } try { media.volume = vol; if (vol <= 0.01) media.muted = true; else { media.muted = false; media.volume = vol; } media.playbackRate = rate; } catch (e) {} cxClickVideoPlayButton(doc); if (media.paused && !media.ended) { media.muted = true; const p = media.play(); if (p && typeof p.catch === 'function') p.catch(() => {}); } if (media.duration > 0 && !media.paused) { const pct = Math.min(100, Math.round(media.currentTime / media.duration * 100)); if (pct % 10 === 0 || pct >= 95) { cxLog('播放中 ' + String(name).slice(0, 14) + ' ' + pct + '%'); } } }, 1500); } function cxProcessVideo(iframeDom, obj) { if (!SettingsManager.get('handleVideo')) { cxLog('未开启视频处理,跳过'); cxComplete(); return; } if (obj.isPassed === true) { cxLog('视频已完成,跳过'); cxComplete(); return; } const mode = SettingsManager.get('videoMode') || 'normal'; if (mode === 'simulate') { const name = (obj.property && obj.property.name) || '视频'; cxGetVideoStatus(obj, (result) => { if (result.success) { cxSimulateVideoReport(result, obj, iframeDom); } else { cxLog('获取视频信息失败,降级前台播放:' + name); cxNormalPlay(iframeDom, obj); } }, iframeDom); return; } cxNormalPlay(iframeDom, obj); } function cxProcessAudio(iframeDom, obj) { if (!SettingsManager.get('handleAudio')) { cxComplete(); return; } if (obj.isPassed === true) { cxComplete(); return; } const name = (obj.property && obj.property.name) || '音频'; cxLog('播放音频:' + name); cxNormalPlay(iframeDom, obj); } function cxParseParams() { try { const scripts = document.scripts; for (let i = 0; i < scripts.length; i++) { const html = scripts[i].innerHTML; if (html.indexOf('mArg = "";') !== -1 && html.indexOf('==UserScript==') === -1) { const s = html.replace(/\s/g, ''); const m = s.match(/try\{mArg=(.*?);\}catch/); return m ? m[1] : null; } } } catch (e) {} return null; } function cxWait(sel, cb, timeout) { timeout = timeout || 30000; const start = Date.now(); const t = setInterval(() => { if ($$(sel).length) { clearInterval(t); cb(); } else if (Date.now() - start > timeout) { clearInterval(t); cb(); } }, 500); } function cxProcessDoc(obj, kind) { if (cxIsPassed(obj)) { cxComplete(); return; } const d = CX.defaults || {}; const jobId = obj.property.jobid, jtoken = obj.jtoken; const base = location.protocol + '//' + location.host + '/ananas/job'; let url = kind === 'document' ? base + '/document' : (kind === 'read' ? base + '/readv2' : base); url += '?jobid=' + jobId + '&knowledgeid=' + d.knowledgeid + '&courseid=' + d.courseid + '&clazzid=' + d.clazzId + '&jtoken=' + jtoken + '&_dc=' + Date.now(); GM_xmlhttpRequest({ method: 'GET', url: url, onload() { cxLog('资料任务完成'); cxComplete(); }, onerror() { CX.processing = false; setTimeout(() => cxProcessDoc(obj, kind), 3000); } }); } function cxBuildTaskQueue(all) { CX.queue = []; CX.frames = []; CX.attachIndices = []; const containers = $$('.wrap .ans-cc .ans-attach-ct'); for (let i = 0; i < all.length; i++) { if (!cxIsTaskDone(all[i], i)) { CX.queue.push(all[i]); CX.attachIndices.push(i); let iframe = null; if (containers.length > 1 && i < containers.length) { iframe = containers[i].querySelector('iframe'); } else if (containers[0]) { iframe = containers[0].querySelector('iframe'); } CX.frames.push(iframe); } } return CX.queue.length; } function cxRebuildQueueAndDispatch() { const all = cxGetAttachments(); const n = cxBuildTaskQueue(all); if (n > 0) { CX.studyMode = false; CX.processing = false; setTimeout(cxDispatch, 800); } } async function cxDispatch() { if (CX.processing) return; if (!CX.queue.length) { CX.processing = false; const pending = cxCountPendingAttachments(); if (pending > 0 && (CX._retryCount || 0) < 3) { CX._retryCount = (CX._retryCount || 0) + 1; cxLog('仍有 ' + pending + ' 个任务点,重新加载队列(' + CX._retryCount + '/3)…'); cxRebuildQueueAndDispatch(); return; } CX._retryCount = 0; if (pending === 0 && !cxNeedsChapterQuizWork()) { cxLog('本节任务已完成'); cxNavigateNext(); } return; } cxCleanup(); CX.processing = true; CX._iframeWait = 0; CX.studyMode = false; const task = CX.queue[0]; const attachIdx = (CX.attachIndices && CX.attachIndices.length) ? CX.attachIndices[0] : 0; const taskName = (task.property && (task.property.name || task.property.title)) || '任务'; const type = task.type || (task.property && task.property.module); const mode = SettingsManager.get('videoMode') || 'normal'; cxClickTaskByName(taskName); cxClickAttachTabByIndex(attachIdx); const dom = await cxSwitchToAttachTab(attachIdx, task); let iframe = dom || (CX.frames.length ? CX.frames[0] : null) || cxResolveIframeByIndex(attachIdx); if (CX.frames.length) CX.frames[0] = iframe; cxLog(((iframe || (type === 'video' && mode === 'simulate')) ? '处理任务:' : '切换任务点:') + taskName); if (type === 'video') cxProcessVideo(iframe, task); else if (type === 'audio') cxProcessAudio(iframe, task); else if (type === 'document') cxProcessDoc(task, 'document'); else if (type === 'read') cxProcessDoc(task, 'read'); else if (type === 'insertbook') cxProcessDoc(task, 'book'); else if (type === 'workid') cxProcessWork(iframe, task); else { cxLog('跳过任务类型: ' + type); cxComplete(); } } function cxProcessWork(iframeDom, task) { if (!SettingsManager.get('handleQuiz')) { cxLog('未开启自动答题,跳过章节测验'); cxComplete(); return; } if (cxIsChapterQuizDone(iframeDom)) { CX.quizWaiting = false; cxLog('章节测验已完成'); cxComplete(); return; } CX.quizWaiting = true; clearWorkQuizDoneFlag(); try { if (iframeDom && iframeDom.contentWindow) { iframeDom.contentWindow.__bookwk_work_done = false; } } catch (e) {} cxLog('章节测验任务点,等待自动答题…'); try { if (iframeDom) iframeDom.scrollIntoView({ block: 'center', behavior: 'smooth' }); } catch (e) {} try { decryptCxFont(); } catch (e) {} const jobId = task.jobid; const start = Date.now(); const maxWait = 30 * 60 * 1000; const poll = () => { if (Date.now() - start > maxWait) { CX.quizWaiting = false; cxLog('章节测验等待超时,继续下一节'); cxComplete(); return; } if (cxIsChapterQuizDone(iframeDom)) { CX.quizWaiting = false; cxLog('章节测验已完成'); cxComplete(); return; } if (cxIsPassed(task)) { CX.quizWaiting = false; cxLog('章节测验已完成'); cxComplete(); return; } const raw = cxParseParams(); if (raw && raw !== '$mArg') { try { const data = JSON.parse(raw); const att = (data.attachments || []).find(t => t.jobid === jobId); if (att && cxIsPassed(att)) { CX.quizWaiting = false; cxLog('章节测验已完成'); cxComplete(); return; } } catch (e) {} } let done = false; try { done = !!(unsafeWindow.top && unsafeWindow.top.__bookwk_work_done); } catch (e) {} if (!done) { try { done = !!unsafeWindow.__bookwk_work_done; } catch (e) {} } if (!done) { try { if (iframeDom && iframeDom.contentWindow) { done = !!iframeDom.contentWindow.__bookwk_work_done; } } catch (e) {} } if (done) { CX.quizWaiting = false; cxLog('章节测验答题完成'); setTimeout(cxComplete, 2000); return; } if (iframeDom) { try { const idoc = iframeDom.contentDocument; try { decryptCxFont(); } catch (e) {} if (idoc && idoc.querySelector('.saveWorkDone, .SubmitSucc, .workDone, .submitSucc')) { CX.quizWaiting = false; cxLog('章节测验已提交'); setTimeout(cxComplete, 2000); return; } } catch (e) {} } setTimeout(poll, 2500); }; poll(); } function cxComplete() { cxCleanup(); CX._iframeWait = 0; if (CX.studyMode) { if (CX._currentMid && CX.completedMids) { CX.completedMids.add(CX._currentMid); CX._currentMid = null; } CX.processing = false; if (typeof CX._jobDoneResolve === 'function') { const done = CX._jobDoneResolve; CX._jobDoneResolve = null; done(); } return; } if (CX.queue.length > 0) { const doneIdx = CX.attachIndices[0]; const doneTask = CX.queue[0]; if (doneTask && CX.completedMids) { CX.completedMids.add(cxAttMid(doneTask, doneIdx)); } CX.queue.shift(); } if (CX.frames.length > 0) CX.frames.shift(); if (CX.attachIndices.length > 0) CX.attachIndices.shift(); CX.processing = false; if (CX.queue.length > 0) { cxLog('剩余 ' + CX.queue.length + ' 个任务点…'); setTimeout(cxDispatch, 1200); } else { const pending = cxCountPendingAttachments(); if (pending > 0 && (CX._retryCount || 0) < 3) { CX._retryCount = (CX._retryCount || 0) + 1; cxLog('仍有 ' + pending + ' 个任务点,重新加载队列(' + CX._retryCount + '/3)…'); cxRebuildQueueAndDispatch(); return; } CX._retryCount = 0; if (pending === 0 && !cxNeedsChapterQuizWork()) { cxLog('本节任务已完成'); cxNavigateNext(); } } } function cxNavigateNext() { if (CX.quizWaiting) { cxLog('章节测验进行中,暂不跳转下一节'); return; } if (!SettingsManager.get('videoAutoNext')) { cxLog('本节完成(未开启自动下一节)'); return; } if (CX.jumping) return; CX.jumping = true; let btn = null; try { const td = unsafeWindow.top.document; btn = td.querySelector('#prevNextFocusNext:not(.disabled)') || td.querySelector('.jb_btn.jb_btn_next:not(.disabled)') || td.querySelector('#mainid > .prev_next.next:not(.disabled)') || td.querySelector('.prev_next.next:not(.disabled)'); } catch (e) {} if (btn) { cxLog('3秒后跳转下一节'); panel.clearRecords(); setTimeout(() => { try { btn.click(); } catch (e) {} CX.jumping = false; }, 3000); } else { cxLog('已是最后一节'); CX.jumping = false; } } function cxHandleCards(attempt) { attempt = attempt || 0; const raw = cxParseParams(); let data = null; if (raw && raw !== '$mArg') { try { data = JSON.parse(raw); } catch (e) { data = null; } } const all = data ? (data.attachments || []) : []; if (data) CX.defaults = data.defaults || {}; // 任务参数未就绪(网页请求过快时常见):先重试几次 if (!data || !all.length) { if (attempt < 4) { cxLog('任务点未就绪,重试中(' + (attempt + 1) + '/4)…'); setTimeout(() => cxHandleCards(attempt + 1), 2000); return; } cxNoTaskFallback(); return; } cxWait('.wrap .ans-cc .ans-attach-ct', () => { try { if (unsafeWindow.top.checkJob) unsafeWindow.top.checkJob = function () { return false; }; } catch (e) {} if (!CX.completedMids) CX.completedMids = new Set(); CX._retryCount = 0; CX.studyMode = false; const n = cxBuildTaskQueue(all); if (n > 0) { cxLog('发现 ' + n + ' 个待完成任务'); cxDispatch(); } else if (cxNeedsChapterQuizWork()) { const iframe = cxFindChapterQuizIframe() || ($$('.wrap .ans-cc .ans-attach-ct')[0] && $$('.wrap .ans-cc .ans-attach-ct')[0].querySelector('iframe')); cxLog(cxHasPendingWorkQuiz() ? '检测到未完成章节测验,等待自动答题…' : '无黄色任务点,强制答题章节测验…'); CX.studyMode = true; CX._jobDoneResolve = () => { CX.studyMode = false; if (cxCountPendingAttachments() === 0) { cxLog('本节任务已完成'); cxNavigateNext(); } }; cxProcessWork(iframe, { jobid: 'pending' }); } else { cxLog('本节任务已全部完成'); setTimeout(cxNavigateNext, 2000); } }); } // 加载过快可能导致读不到任务点:把倍速强制调回 1 倍,减小页面因高倍速跳转过快的影响 function cxResetRate() { try { if (Number(SettingsManager.get('videoRate')) !== 1) { SettingsManager.set('videoRate', 1); const inp = $('#bookwk-set [data-key="videoRate"]'); if (inp) inp.value = 1; cxLog('检测到加载过快/无任务点,已将倍速自动调回 1 倍'); } } catch (e) {} } // 多次重试仍无任务点:先自动刷新一次排除“加载过快”,刷新后仍无则判定确无任务并跳转下一节 function cxNoTaskFallback() { cxResetRate(); const key = 'longlong_cx_reloaded_' + location.pathname + location.search; let reloaded = false; try { reloaded = sessionStorage.getItem(key) === '1'; } catch (e) {} if (!reloaded) { try { sessionStorage.setItem(key, '1'); } catch (e) {} cxLog('未检测到任务点,正在刷新页面重试…'); setTimeout(() => { try { location.reload(); } catch (e) {} }, 1500); return; } try { sessionStorage.removeItem(key); } catch (e) {} if (cxNeedsChapterQuizWork()) { const iframe = cxFindChapterQuizIframe() || ($$('.wrap .ans-cc .ans-attach-ct')[0] && $$('.wrap .ans-cc .ans-attach-ct')[0].querySelector('iframe')); cxLog(cxHasPendingWorkQuiz() ? '检测到章节测验,等待自动答题…' : '无黄色任务点,强制答题章节测验…'); cxProcessWork(iframe, { jobid: 'pending' }); return; } cxLog('本节确无任务点,自动跳转下一节'); cxNavigateNext(); } function initChaoxingCourse() { const host = location.host; if (host.indexOf('chaoxing') < 0 && host.indexOf('xuexitong') < 0) return; const run = () => { if (/\/knowledge\/cards/.test(location.href)) { panel.init(); cxHandleCards(); } else if (/\/mycourse\/studentstudy/.test(location.href)) { panel.init(); cxLog('学习通课程页已就绪'); } }; run(); let last = location.href; setInterval(() => { if (location.href !== last) { last = location.href; panel.clearRecords(); cxCleanup(); setTimeout(run, 3000); } }, 2000); } const panel = { rows: [], _doc: null, getDoc() { if (panel._doc) return panel._doc; let d = document; try { if (window.top && window.top !== window.self) { void window.top.document.body; // 跨域会抛错 d = window.top.document; } } catch (e) { d = document; } panel._doc = d; return d; }, applyPosition(box, doc) { try { const raw = GM_getValue('bookwk_panel_pos', ''); if (!raw) return; const pos = typeof raw === 'string' ? JSON.parse(raw) : raw; const left = Number(pos.left); const top = Number(pos.top); if (isNaN(left) || isNaN(top)) return; const vw = doc.documentElement.clientWidth; const vh = doc.documentElement.clientHeight; box.style.left = Math.max(0, Math.min(left, vw - 60)) + 'px'; box.style.top = Math.max(0, Math.min(top, vh - 30)) + 'px'; box.style.right = 'auto'; } catch (e) {} }, savePosition(box) { try { const left = parseFloat(box.style.left); const top = parseFloat(box.style.top); if (isNaN(left) || isNaN(top)) return; GM_setValue('bookwk_panel_pos', JSON.stringify({ left, top })); } catch (e) {} }, enableDrag(box, doc) { const hd = box.querySelector('.hd'); if (!hd) return; let dragging = false, sx = 0, sy = 0, ox = 0, oy = 0; hd.addEventListener('mousedown', (e) => { if (e.target.closest('#bookwk-gear')) return; const rect = box.getBoundingClientRect(); box.style.left = rect.left + 'px'; box.style.top = rect.top + 'px'; box.style.right = 'auto'; dragging = true; sx = e.clientX; sy = e.clientY; ox = rect.left; oy = rect.top; e.preventDefault(); }); doc.addEventListener('mousemove', (e) => { if (!dragging) return; const vw = doc.documentElement.clientWidth, vh = doc.documentElement.clientHeight; let nx = Math.max(0, Math.min(ox + (e.clientX - sx), vw - 60)); let ny = Math.max(0, Math.min(oy + (e.clientY - sy), vh - 30)); box.style.left = nx + 'px'; box.style.top = ny + 'px'; }); doc.addEventListener('mouseup', () => { if (dragging) panel.savePosition(box); dragging = false; }); }, init() { const doc = panel.getDoc(); if (doc.getElementById('bookwk-panel')) return; if (!doc.getElementById('bookwk-style')) { const style = doc.createElement('style'); style.id = 'bookwk-style'; style.textContent = ` #bookwk-panel{position:fixed;right:16px;top:80px;width:300px;max-height:70vh;overflow:auto; background:#fff;border:1px solid #dcdfe6;border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,.12); font:12px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif; color:#606266;z-index:2147483646} #bookwk-panel .hd{padding:10px 12px;background:#409eff;color:#fff;font-weight:600;border-radius:8px 8px 0 0;cursor:move;user-select:none} #bookwk-panel .bd{padding:10px 12px} #bookwk-panel .row{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px} #bookwk-panel button{border:0;border-radius:4px;padding:6px 10px;cursor:pointer;color:#fff} #bookwk-panel .primary{background:#409eff} #bookwk-panel .warn{background:#e6a23c} #bookwk-panel .danger{background:#f56c6c} #bookwk-panel .tip{background:#f0f9eb;border:1px solid #e1f3d8;color:#67c23a;padding:8px;border-radius:4px;margin-bottom:8px;word-break:break-all} #bookwk-panel table{width:100%;border-collapse:collapse} #bookwk-panel th,#bookwk-panel td{border-bottom:1px solid #ebeef5;padding:6px 4px;text-align:left;vertical-align:top} #bookwk-panel tr.ok{background:#f0f9eb} #bookwk-panel tr.bad{background:#fdf6ec} #bookwk-panel tr.review{background:#ecf5ff} #bookwk-panel input[type=password],#bookwk-panel input[type=number]{width:100%;box-sizing:border-box;padding:6px 8px;border:1px solid #dcdfe6;border-radius:4px} #bookwk-panel .hd .gear{float:right;cursor:pointer;font-weight:400} #bookwk-panel .set{padding:0 12px 10px;display:none;border-bottom:1px solid #ebeef5} #bookwk-panel .set.open{display:block} #bookwk-panel .set label{display:flex;align-items:center;gap:6px;margin:6px 0;cursor:pointer} #bookwk-panel .set .num{display:flex;align-items:center;gap:6px;margin:6px 0} #bookwk-panel .set .num input{width:90px} `; (doc.head || doc.documentElement).appendChild(style); } const box = doc.createElement('div'); box.id = 'bookwk-panel'; box.innerHTML = `
龙龙 · 学习通/智慧树助手 v1.3.35 (F11隐藏) ⚙ 设置
随机填空词
章节测验/作业答完后
答完等待(秒)
视频模式
上报间隔(秒,模拟模式)
视频弹题
视频倍速(1-16,建议1-2)
视频音量(0-1)
答题间隔(ms)
填充间隔(ms)
龙龙免费题库 · 交流论坛 www.qsxxw.top
#题目答案
`; doc.body.appendChild(box); panel.applyPosition(box, doc); if (!GM_getValue('bookwk_panel_pos', '')) panel.moveToEdge(); panel.enableDrag(box, doc); const q = (s) => box.querySelector(s); // 绑定设置项 box.querySelectorAll('#bookwk-set [data-key]').forEach(inp => { const key = inp.getAttribute('data-key'); if (inp.type === 'checkbox') { inp.checked = !!SettingsManager.get(key); inp.onchange = () => { SettingsManager.set(key, inp.checked ? 1 : 0); if (key === 'showBox') panel.applyShowBox(); }; } else if (inp.tagName === 'SELECT') { const val = key === 'quizSubmitMode' ? String(getQuizSubmitMode()) : String(SettingsManager.get(key)); inp.value = val; inp.onchange = () => { if (key === 'videoMode') { const prevMode = SettingsManager.get('videoMode') || 'normal'; if (inp.value === 'normal' && prevMode === 'simulate') { cxSwitchToNormalAndReload(box); return; } if (inp.value === 'simulate' && prevMode === 'normal') { cxSwitchToSimulateModeAndReload(box); return; } } SettingsManager.set(key, inp.value); if (key === 'videoMode' && inp.value === 'normal') { cxEnforceNormalVideoRate(box); } }; } else if (inp.type === 'text') { inp.value = SettingsManager.get(key); inp.onchange = () => SettingsManager.set(key, inp.value); } else { let initRate = Number(SettingsManager.get(key)) || SettingsManager._defaults[key]; if (key === 'videoRate') { const mode = SettingsManager.get('videoMode') || 'normal'; if (mode === 'normal' && initRate > 1) { initRate = 1; SettingsManager.set('videoRate', 1); } } inp.value = initRate; inp.onchange = () => { let v = Number(inp.value) || SettingsManager._defaults[key]; if (key === 'videoRate') { v = _clampRate(v); const mode = SettingsManager.get('videoMode') || 'normal'; if (mode === 'normal') { if (v > 1) { cxSwitchToSimulateAndReload(box, v); return; } inp.value = v; SettingsManager.set(key, v); return; } inp.value = v; SettingsManager.set(key, v); if (cxApplySimulateRate(v)) { panel.tip('模拟上报倍速已更新为 ' + v + 'x'); } else { panel.tip('倍速已保存,下个视频任务生效'); } return; } else if (key === 'videoVolume') { v = _clampVolume(v); inp.value = v; } SettingsManager.set(key, v); }; } }); if ((SettingsManager.get('videoMode') || 'normal') === 'normal') { cxEnforceNormalVideoRate(box); } q('#bookwk-gear').onclick = () => q('#bookwk-set').classList.toggle('open'); panel.applyShowBox(); q('#bookwk-token').value = GLOBAL.token; q('#bookwk-save').onclick = () => { GLOBAL.token = q('#bookwk-token').value.trim() || DEFAULT_TOKEN; GM_setValue('bookwk_token', GLOBAL.token); panel.tip('Token 已保存'); }; q('#bookwk-toggle').onclick = () => { setPaused(!isPaused()); const p = isPaused(); if (!p) _pausedForFace = false; q('#bookwk-toggle').textContent = p ? '继续答题' : '暂停答题'; panel.tip(p ? '已暂停' : '继续自动答题'); }; q('#bookwk-clear').onclick = () => panel.clearRecords(); panel.bindHotkey(); }, _hotkeyBound: false, togglePanel() { const box = panel.getDoc().getElementById('bookwk-panel'); if (box) box.style.display = (box.style.display === 'none') ? '' : 'none'; }, bindHotkey() { if (panel._hotkeyBound) return; panel._hotkeyBound = true; const handler = (e) => { if (e.key === 'F11' || e.keyCode === 122) { e.preventDefault(); e.stopPropagation(); panel.togglePanel(); } }; try { document.addEventListener('keydown', handler, true); } catch (e) {} try { if (unsafeWindow.top && unsafeWindow.top.document && unsafeWindow.top.document !== document) { unsafeWindow.top.document.addEventListener('keydown', handler, true); } } catch (e) {} }, applyShowBox() { const table = panel.getDoc().getElementById('bookwk-table'); if (table) table.style.display = SettingsManager.get('showBox') ? '' : 'none'; }, moveToEdge() { if (!SettingsManager.get('panelAvoidBlock')) return; if (GM_getValue('bookwk_panel_pos', '')) return; const doc = panel.getDoc(); const box = doc.getElementById('bookwk-panel'); if (!box) return; const vw = doc.documentElement.clientWidth; const bw = box.offsetWidth || 300; box.style.left = Math.max(4, vw - bw - 4) + 'px'; box.style.top = '60px'; box.style.right = 'auto'; }, clearRecords() { panel.rows = []; const tbody = panel.getDoc().getElementById('bookwk-tbody'); if (tbody) tbody.innerHTML = ''; }, tip(msg) { const el = panel.getDoc().getElementById('bookwk-tip'); if (el) el.textContent = msg; }, rowClass(ok) { if (ok === 'review') return 'review'; return ok ? 'ok' : 'bad'; }, push(idx, question, answer, ok) { const cls = panel.rowClass(ok); const existIdx = panel.rows.findIndex(r => r.idx === idx); const doc = panel.getDoc(); const tbody = doc.getElementById('bookwk-tbody'); if (existIdx >= 0) { panel.rows[existIdx] = { idx, question, answer, ok }; if (tbody) { const trs = tbody.querySelectorAll('tr'); for (let i = 0; i < trs.length; i++) { const tr = trs[i]; if (tr.children[0] && String(tr.children[0].textContent) === String(idx)) { tr.className = cls; tr.innerHTML = '' + idx + '' + question.slice(0, 80) + '' + answer + ''; return; } } } } panel.rows.push({ idx, question, answer, ok }); if (!tbody) return; const tr = doc.createElement('tr'); tr.className = cls; tr.innerHTML = '' + idx + '' + question.slice(0, 80) + '' + answer + ''; tbody.appendChild(tr); } }; function insertAnswerTag($item, answerText) { try { if ($item.querySelector('.longlong-answer-tag')) return; const tag = document.createElement('div'); tag.className = 'longlong-answer-tag'; tag.style.cssText = 'margin:6px 0;padding:6px 10px;background:#f0f9eb;border:1px solid #c2e7b0;border-radius:4px;color:#529b2e;font-size:13px;word-break:break-all;'; tag.textContent = '【龙龙答案】' + answerText; $item.appendChild(tag); } catch (e) {} } async function tryAutoSave() { const saveTexts = /^(暂时保存|临时保存|保存草稿|暂存)$/; for (const doc of collectAccessibleDocs()) { const btn = Array.from(doc.querySelectorAll('a,button,input[type=button],input[type=submit],div,span')) .find(b => saveTexts.test((b.textContent || b.value || '').trim())); if (btn) { fireClick(btn); try { btn.click(); } catch (e) {} return true; } } return false; } async function confirmSubmitDialogs(forceConfirm) { if (!forceConfirm && !SettingsManager.get('forceSubmit')) return true; const confirmTexts = /^(确定|确认|是|确定提交|提交)$/; for (let i = 0; i < 8; i++) { await sleep(400); for (const doc of collectAccessibleDocs()) { const layers = doc.querySelectorAll( '.layui-layer-dialog, .layui-layer[type="dialog"], .el-message-box, .el-dialog, .ant-modal, [class*="dialog"], [class*="modal"]' ); const scopes = layers.length ? Array.from(layers) : [doc.body]; for (const scope of scopes) { if (!scope || (scope.style && scope.style.display === 'none')) continue; const hint = scope.textContent || ''; if (layers.length && !/确认提交|确定提交|确认要提交|提示/.test(hint)) continue; const btn = Array.from(scope.querySelectorAll('a,button,.el-button,.layui-layer-btn a,.layui-layer-btn0,.layui-layer-btn1,span')) .find(b => { const t = (b.textContent || b.value || '').trim(); return confirmTexts.test(t) && t !== '取消'; }); if (btn) { fireClick(btn); try { btn.click(); } catch (e) {} return true; } } } } return true; } async function tryAutoSubmit(forceConfirm) { const submitTexts = /^(交卷|提交|提交试卷|确定提交|保存并提交|提交作业)$/; let submitBtn = null; for (const doc of collectAccessibleDocs()) { const btn = Array.from(doc.querySelectorAll('a,button,input[type=button],input[type=submit],div,span')) .find(b => submitTexts.test((b.textContent || b.value || '').trim())); if (btn) { submitBtn = btn; break; } } if (!submitBtn) return false; fireClick(submitBtn); try { submitBtn.click(); } catch (e) {} return confirmSubmitDialogs(forceConfirm); } async function processOne(data, handlers) { const res = await searchAnswer(data.question); if (!res.success) { const random = await tryRandomWork(data); if (random && random.ok) { const qNo = data.qNo != null ? data.qNo : (GLOBAL.index + 1); panel.push(qNo, data.question, random.display, true); panel.tip('第' + qNo + '题:未搜到答案,已随机作答'); if (typeof handlers.fillFinish === 'function') await handlers.fillFinish(random, data); return true; } const qTip = data.qNo != null ? data.qNo : (GLOBAL.index + 1); panel.push(qTip, data.question, res.msg, false); panel.tip('第' + qTip + '题:' + res.msg); return false; } const type = normalizeType(data.type); const answers = parseAnswers(res.answer, type, data.options); const review = !!SettingsManager.get('reviewMode'); let result = { ok: false, display: res.answer }; if (type === 2 || type === 4 || type === 5 || type === 6) { let filled = false; if (!review) { if (data.inputs && data.inputs.length) { filled = await fillBlank(answers, data.inputs, handlers && handlers.fill); } if (!filled) { filled = await fillSubjectiveAnswer(answers, data.$item, type, handlers && handlers.fill); } } result = { ok: filled, display: answers.join(' | ') }; } else if (type === 11) { let filled = false; if (!review) filled = await fillLineQuestion(data.$item, answers); result = { ok: filled, display: answers.join(' | ') }; } else if (type === 14) { let filled = false; if (!review) filled = await fillCompositeChoiceQuestion(data.$item, answers, '.filling_answer'); result = { ok: filled, display: answers.join(' | ') }; } else if (type === 15) { let filled = false; if (!review) filled = await fillCompositeChoiceQuestion(data.$item, answers, '.reading_answer'); result = { ok: filled, display: answers.join(' | ') }; } else if (isChoiceType(type)) { await ensureFontDecrypted(data.$item, 4); const scene = data.cxScene || getCxQuizScene(); const fresh = cxCollectOptionsForScene(data.$item, scene); if (fresh && fresh.optionNodes.length >= 2) { data.options = fresh.options; data.optionNodes = fresh.optionNodes; } const nodes = (data.optionNodes && data.optionNodes.length) ? data.optionNodes : []; let indices = mapIndicesToOptionNodes( matchOptionIndexWithFinalize(answers, data.options, type, nodes), nodes ); if (!indices.length && /^[A-Ha-h]$/.test(String(res.answer || '').trim())) { const letterIdx = String(res.answer).trim().toUpperCase().charCodeAt(0) - 65; if (letterIdx >= 0 && letterIdx < nodes.length) indices = [letterIdx]; } let verified = false; if (!review && indices.length && nodes.length) { verified = await fillCxChoice(indices, nodes, type, data.$item, scene); } if (!verified && indices.length) { console.warn('[龙龙助手] 选题失败', { q: data.question, answer: res.answer, options: data.options, indices, scene }); } const labels = indices.length ? indices.map(i => cxOptionText(nodes[i]) || (isTiMuItem(data.$item) ? cxTiMuOptionText(nodes[i]) : '') || data.options[i]) : answers; result = { ok: verified, display: indices.length ? indices.map(i => String.fromCharCode(65 + i)).join('') + ' / ' + labels.join(' | ') : (Array.isArray(labels) ? labels.join(' | ') : String(labels)) }; } const qNo = data.qNo != null ? data.qNo : (GLOBAL.index + 1); if (SettingsManager.get('insertAnswer') && data.$item && result.ok && !review) { insertAnswerTag(data.$item, res.answer); } panel.push(qNo, data.question, result.display, review ? 'review' : result.ok); panel.tip('第' + qNo + '题完成:' + (review ? '复习展示' : result.ok ? '已填充' : '需人工检查')); if (typeof handlers.fillFinish === 'function' && !review) await handlers.fillFinish(result, data); return review ? true : result.ok; } function shouldExecuteWorker() { if (resolveQuizDoc() === document) return true; return !!document.querySelector('.TiMu, .clearfix .TiMu, .questionLi, .question-item'); } const WORKER_REGISTRY = []; function resetWorkerState() { GLOBAL.matched = false; GLOBAL.index = 0; GLOBAL._retry = 0; GLOBAL._noItemsRetry = 0; GLOBAL._loopSession = (GLOBAL._loopSession || 0) + 1; } function rematchWorkers() { resetWorkerState(); for (const config of WORKER_REGISTRY) { startWorker(config); if (GLOBAL.matched) break; } } async function startWorker(config) { if (GLOBAL.matched) return; const match = typeof config.match === 'function' ? config.match() : !!config.match; if (!match) return; if (!shouldExecuteWorker()) return; GLOBAL.matched = true; const waitReady = () => { if (typeof config.intv === 'function') return config.intv(); if (config.root) return $$(config.root).length > 0; return true; }; const loopGen = GLOBAL._loopSession || 0; const readyTimer = setInterval(async () => { if ((GLOBAL._loopSession || 0) !== loopGen) { clearInterval(readyTimer); return; } if (!waitReady()) return; clearInterval(readyTimer); panel.init(); panel.tip(config.name + ' 已匹配,准备答题…'); if (typeof config.hook === 'function') config.hook(); if (config.cxScene) GLOBAL._cxScene = config.cxScene; await sleep(GLOBAL.delay); await loop(config, loopGen); }, 1000); } async function runWorker(config) { if (WORKER_REGISTRY.indexOf(config) < 0) WORKER_REGISTRY.push(config); return startWorker(config); } function getQuizSubmitMode() { var m = SettingsManager.get('quizSubmitMode'); if (m !== undefined && m !== null && String(m) !== '') return String(m); if (!SettingsManager.get('autoSubmit')) return 'nomove'; return SettingsManager.get('forceSubmit') ? 'force' : '100'; } function calcQuizUploadable(finishedRate, mode) { if (mode === 'force') return true; if (mode === 'save' || mode === 'nomove') return false; var th = parseFloat(mode); if (!isNaN(th)) return finishedRate >= th; return false; } function collectAccessibleWindows() { const wins = []; const seen = new Set(); const walkWin = (win, depth) => { if (!win || depth > 6 || seen.has(win)) return; seen.add(win); wins.push(win); let doc; try { doc = win.document; } catch (e) { return; } if (!doc) return; doc.querySelectorAll('iframe').forEach(frame => { try { walkWin(frame.contentWindow, depth + 1); } catch (e) {} }); }; try { walkWin(window.top, 0); } catch (e) { walkWin(window, 0); } if (!seen.has(window)) walkWin(window, 0); return wins; } function getChapterQuizFrameWindow() { const candidates = []; const push = (win) => { if (win && candidates.indexOf(win) < 0) candidates.push(win); }; try { if (typeof unsafeWindow.btnBlueSubmit === 'function' || typeof unsafeWindow.noSubmit === 'function') { push(unsafeWindow); } } catch (e) {} try { if (window !== unsafeWindow && (typeof window.btnBlueSubmit === 'function' || typeof window.noSubmit === 'function')) { push(window); } } catch (e) {} for (const win of collectAccessibleWindows()) push(win); for (const win of candidates) { try { if (typeof win.btnBlueSubmit === 'function' || typeof win.noSubmit === 'function') { return win; } } catch (e) {} } return null; } /** 通知父页章节测验一轮结束(与门槛/是否提交无关,避免挂机卡在「等待自动答题」) */ function signalRoundDone(config) { if (typeof config.onRoundDone === 'function') { try { config.onRoundDone(true); } catch (e) {} } } /** * 对齐 OCS 共享课:提交/暂存前遍历答题卡并点「下一题」,确保答案写入服务器 */ async function chapterQuizPersistAnswers(frameWin, doc) { const d = doc || (frameWin && frameWin.document) || document; const cardItems = d.querySelectorAll('.answerCard_list ul li, .answerCard_list1 li, .answerCard li'); const timuItems = d.querySelectorAll('.TiMu, .clearfix .TiMu'); const total = cardItems.length || timuItems.length; if (!total) return; panel.tip('正在逐题暂存答案(' + total + '题)…'); for (let i = 0; i < total; i++) { if (cardItems.length) { const card = cardItems.item(i); if (card) { fireClick(card); try { card.click(); } catch (e) {} await sleep(500); } } else if (timuItems[i]) { try { timuItems[i].scrollIntoView({ block: 'center', behavior: 'auto' }); } catch (e) {} const saveBtn = timuItems[i].querySelector('[onclick*="saveQuestion"], [onclick*="SaveQuestion"]'); if (saveBtn) { fireClick(saveBtn); try { saveBtn.click(); } catch (e) {} } await sleep(300); } const nextBtn = d.querySelector('div.examPaper_box > div.switch-btn-box > button:nth-child(2)') || Array.from(d.querySelectorAll('a, button, .jb_btn, .nextDiv a, .nextDiv .jb_btn')) .find(b => /下一题|下一页/.test((b.textContent || b.value || '').trim())); if (nextBtn) { fireClick(nextBtn); try { nextBtn.click(); } catch (e) {} await sleep(2000); } else if (frameWin && typeof frameWin.getTheNextQuestion === 'function') { try { frameWin.getTheNextQuestion(1); } catch (e) {} await sleep(2000); } } } async function chapterQuizIframeSave(frameWin) { if (!frameWin) return false; try { frameWin.alert = function() {}; if (typeof frameWin.noSubmit === 'function') { frameWin.noSubmit(); return true; } } catch (e) {} return false; } function isChapterQuizAlreadySubmitted(frameWin, doc) { const docs = []; if (doc) docs.push(doc); if (frameWin && frameWin.document && frameWin.document !== doc) docs.push(frameWin.document); try { if (document.body) docs.push(document); } catch (e) {} for (const d of docs) { if (!d || !d.body) continue; if (d.querySelector('.testTit_status_complete, .saveWorkDone, .SubmitSucc, .workDone, .submitSucc')) return true; const status = d.querySelector('.testTit_status'); if (status && /complete|done|finish/i.test(status.className || '')) return true; if (/任务点已完成|测验已完成|提交成功|已提交/.test(d.body.innerText || '')) return true; } return false; } async function chapterQuizIframeSubmit(frameWin) { if (!frameWin) return false; const doc = frameWin.document || document; if (isChapterQuizAlreadySubmitted(frameWin, doc)) return true; if (typeof frameWin.btnBlueSubmit !== 'function') return false; try { frameWin.alert = function() {}; frameWin.btnBlueSubmit(); await sleep(2000); if (typeof frameWin.submitCheckTimes === 'function') { frameWin.submitCheckTimes(); } try { const topWin = frameWin.top || frameWin; if (topWin.$ && typeof topWin.$ === 'function') { topWin.$('#workpop').hide(); } } catch (e) {} await Promise.race([ confirmSubmitDialogs(true), sleep(5000) ]); await sleep(1000); return isChapterQuizAlreadySubmitted(frameWin, doc) || true; } catch (e) { return isChapterQuizAlreadySubmitted(frameWin, doc); } } function normalizeRoundStats(stats) { if (typeof stats === 'boolean') { var allOk = stats; var total = GLOBAL.roundTotal || 1; var finished = allOk ? total : (GLOBAL.roundFinished || 0); return { allOk: allOk, finished: finished, total: total }; } stats = stats || {}; var total = Number(stats.total); var finished = Number(stats.finished); if (isNaN(total) || total < 1) total = 1; if (isNaN(finished)) finished = stats.allOk !== false ? total : 0; return { allOk: stats.allOk !== false, finished: Math.max(0, Math.min(finished, total)), total: total }; } async function gotoNextSinglePageQuestion(config) { if (!SettingsManager.get('autoNextExam') || typeof config.finished !== 'function') return 'no-auto'; if (SettingsManager.get('examNextDelay')) await sleep(Math.floor(Math.random() * 1500)); const cont = await config.finished(); if (!cont) { await finishQuizRound(config, { finished: GLOBAL.roundFinished || 0, total: GLOBAL.roundTotal || 1, allOk: GLOBAL.roundOk !== false }); return 'end'; } GLOBAL._roundPaginating = true; GLOBAL.index = 0; await sleep(1500); return 'next'; } /** 单页考试翻页:next 继续;end 已在 goto 内收尾;no-auto 等需在本轮结束 */ async function advanceSinglePageOrFinish(config, r) { if (r === 'next') return true; if (r === 'end') return false; await finishQuizRound(config, { finished: GLOBAL.roundFinished || 0, total: GLOBAL.roundTotal || 1, allOk: GLOBAL.roundOk !== false }); return false; } function configAllowsAutoSubmit(config) { if (!config) return false; if (typeof config.allowAutoSubmit === 'function') return !!config.allowAutoSubmit(); return config.allowAutoSubmit !== false; } async function finishQuizRound(config, stats) { stats = normalizeRoundStats(stats); GLOBAL._roundPaginating = false; if (!configAllowsAutoSubmit(config)) { panel.tip('答题结束(考试模式:不自动保存/提交,请自行检查并交卷)'); signalRoundDone(config); return; } var finishedRate = stats.total > 0 ? (stats.finished / stats.total) * 100 : 0; var mode = getQuizSubmitMode(); var stopSec = Number(SettingsManager.get('stopSecondWhenFinish')); if (isNaN(stopSec) || stopSec < 0) stopSec = 3; var uploadable = calcQuizUploadable(finishedRate, mode); var frameWin = getChapterQuizFrameWindow(); var frameDoc = frameWin && frameWin.document ? frameWin.document : null; panel.tip('答题完成 ' + stats.finished + '/' + stats.total + '(' + finishedRate.toFixed(0) + '%),' + stopSec + '秒后处理保存/提交…'); if (stopSec > 0) await sleep(stopSec * 1000); if (mode === 'nomove') { panel.tip('本轮完成(不自动保存/提交),请自行检查'); } else { const isZhs = /zhihuishu|zjooc|hiknow/.test(location.host); if (isChapterQuizAlreadySubmitted(frameWin, frameDoc)) { panel.tip('提交成功,任务点已完成'); } else if (isZhs) { await zhsPersistAnswers(); panel.tip('智慧树答题已处理,请检查提交状态'); } else { // 整页多题(如章节「学习检测」)答案已在页面上,无需逐题点下一题 if (frameWin && frameDoc && !isChapterMultiQuiz()) { panel.tip('正在逐题暂存答案…'); await chapterQuizPersistAnswers(frameWin, frameDoc); } var submitted = false; if (!uploadable) { panel.tip('完成率 ' + finishedRate.toFixed(0) + '%,正在暂时保存…'); submitted = frameWin ? await chapterQuizIframeSave(frameWin) : false; if (!submitted) submitted = await tryAutoSave(); panel.tip(submitted ? '已暂时保存' : '暂存失败,请手动保存'); } else { panel.tip('完成率 ' + finishedRate.toFixed(0) + '%,正在提交…'); try { if (isChapterQuizAlreadySubmitted(frameWin, frameDoc)) { submitted = true; } else if (frameWin) { submitted = await chapterQuizIframeSubmit(frameWin); } if (!submitted) { submitted = await tryAutoSubmit(uploadable || mode === 'force' || SettingsManager.get('forceSubmit')); } } finally { if (isChapterQuizAlreadySubmitted(frameWin, frameDoc)) { panel.tip('提交成功,任务点已完成'); } else { panel.tip(submitted ? '已触发提交,请确认任务点状态' : '提交失败,请手动提交'); } } } } } GLOBAL._roundPaginating = false; signalRoundDone(config); } async function loop(config, loopGen) { loopGen = loopGen != null ? loopGen : (GLOBAL._loopSession || 0); GLOBAL.index = 0; GLOBAL.roundOk = true; GLOBAL.roundFinished = 0; GLOBAL.roundTotal = 0; GLOBAL._roundPaginating = false; GLOBAL._noItemsRetry = 0; while (true) { if ((GLOBAL._loopSession || 0) !== loopGen) return; if (checkSafetyPauses()) { await sleep(800); continue; } refreshQuizDoc(); const items = getQuizItems(config.root); if (!items.length) { GLOBAL._noItemsRetry = (GLOBAL._noItemsRetry || 0) + 1; if (GLOBAL._noItemsRetry < 15) { panel.tip('未找到题目节点,等待加载(' + GLOBAL._noItemsRetry + '/15)…'); await sleep(2000); continue; } panel.tip('未找到题目节点,已停止本轮'); return; } GLOBAL._noItemsRetry = 0; if (GLOBAL.index === 0) { if (GLOBAL._roundPaginating) { GLOBAL.roundTotal = (GLOBAL.roundTotal || 0) + items.length; } else { GLOBAL.roundFinished = 0; GLOBAL.roundTotal = items.length; } } if (!isSinglePageExam() && GLOBAL.index >= items.length) { if (SettingsManager.get('autoNextExam') && typeof config.finished === 'function') { if (SettingsManager.get('examNextDelay')) await sleep(Math.floor(Math.random() * 1500)); const cont = await config.finished(); if (cont) { GLOBAL._roundPaginating = true; GLOBAL.index = 0; await sleep(1500); continue; } } await finishQuizRound(config, { finished: GLOBAL.roundFinished || 0, total: GLOBAL.roundTotal || items.length, allOk: GLOBAL.roundOk !== false }); return; } if (isPaused() || !SettingsManager.get('handleQuiz')) { await sleep(500); continue; } await sleep(GLOBAL.searchDelay * 1000); if (isSinglePageExam() && GLOBAL.index >= items.length) GLOBAL.index = 0; const item = items[GLOBAL.index] || items[0]; if (!item) { panel.tip('未找到当前题目节点,结束本轮'); await finishQuizRound(config, { finished: GLOBAL.roundFinished || 0, total: GLOBAL.roundTotal || items.length, allOk: GLOBAL.roundOk !== false }); return; } await ensureFontDecrypted(item); try { item.scrollIntoView({ block: 'center', behavior: 'auto' }); } catch (e) {} await sleep(300); let data; try { data = config.collect(item, GLOBAL.index); } catch (e) { GLOBAL.roundOk = false; panel.tip('第' + (GLOBAL.index + 1) + '题解析失败:' + e.message + ',跳过继续'); if (isSinglePageExam()) { const r = await gotoNextSinglePageQuestion(config); if (await advanceSinglePageOrFinish(config, r)) continue; return; } GLOBAL.index++; continue; } if (!data || !data.question) { GLOBAL.roundOk = false; panel.tip('第' + (GLOBAL.index + 1) + '题题干为空,跳过继续'); if (isSinglePageExam()) { const r = await gotoNextSinglePageQuestion(config); if (await advanceSinglePageOrFinish(config, r)) continue; return; } GLOBAL.index++; continue; } if (typeof config.wrap === 'function') config.wrap(data, item); const qNo = data.qNo != null ? data.qNo : (GLOBAL.index + 1); panel.tip('正在答第' + qNo + '题…'); const ok = await processOne(data, config); if (ok) { if (!SettingsManager.get('reviewMode')) { GLOBAL.roundFinished = (GLOBAL.roundFinished || 0) + 1; } if (isChapterMultiQuiz()) await sleep(600); GLOBAL._retry = 0; if (isSinglePageExam()) { const r = await gotoNextSinglePageQuestion(config); if (await advanceSinglePageOrFinish(config, r)) continue; return; } GLOBAL.index++; } else { GLOBAL._retry = (GLOBAL._retry || 0) + 1; if (GLOBAL._retry >= 2) { GLOBAL._retry = 0; GLOBAL.roundOk = false; panel.tip('第' + qNo + '题未答上,跳过继续下一题'); if (isSinglePageExam()) { const r = await gotoNextSinglePageQuestion(config); if (await advanceSinglePageOrFinish(config, r)) continue; return; } GLOBAL.index++; } else await sleep(1000); } } } function cxResolveType(item, questionEl, optionNodes, index) { let type; const typeInputs = item.querySelectorAll('input[name^=type], input[name^=answertype]'); const typeIdx = cxTypeInputIndex(); const typeInput = typeInputs.length ? (typeInputs[typeIdx] || typeInputs[0]) : null; if (typeInput && typeInput.value !== '') { const byName = getQuestionType(typeInput.value); if (byName !== undefined) type = byName; else { const n = parseInt(typeInput.value, 10); if (!isNaN(n)) type = CX_TYPE_CODE[n] !== undefined ? CX_TYPE_CODE[n] : n; } } if (type === undefined && isTiMuItem(item)) { const h3 = item.querySelector('h3.mark_name, h3'); if (h3) { const shallow = h3.querySelector('.colorShallow'); const fromShallow = shallow ? getQuestionType(shallow.textContent) : undefined; if (fromShallow !== undefined) type = fromShallow; if (type === undefined) type = getQuestionType(filterImgText(h3)); } } if (type === undefined && item.querySelector('.filling_answer')) type = 14; if (type === undefined && item.querySelector('.reading_answer')) type = 15; if (type === undefined && item.querySelector('.line_answer_ct, .thirdUlList .dept_select')) type = 11; if (type === undefined) { const queType = item.querySelector('input[class^=que-type]'); if (queType && queType.value !== '') { const n = parseInt(queType.value, 10); if (!isNaN(n)) type = CX_TYPE_CODE[n] !== undefined ? CX_TYPE_CODE[n] : n; } } if (type === undefined && questionEl) { const shallow = questionEl.querySelector('.colorShallow'); if (shallow) type = getQuestionType(shallow.textContent); if (type === undefined) type = getQuestionType(filterImgText(questionEl)); } if (type === undefined && optionNodes && optionNodes.length) { const scope = getTiMuOptionScope(item) || item; const radios = scope.querySelectorAll('input[type=radio]'); const checks = scope.querySelectorAll('input[type=checkbox]'); const texts = item.querySelectorAll('textarea'); if (radios.length === 2 && checks.length === 0) type = 3; else if (checks.length > 0) type = 1; else if (radios.length > 2) type = 0; else if (texts.length) type = 4; else type = 0; } return normalizeType(type); } function cxQuestionNo(questionEl, fallback, item) { const fromTitle = parseQuestionNoFromEl(item, questionEl); if (fromTitle) return fromTitle; const curSel = '.topicNumber_list li.current.active, .topicNumber_list li.current, .topicNumber_list li.active, ' + '.NumberCardUl li.current, .cardUl li.current, .cardUl li.cur, #cardUl li.current, ' + '.rightPartNumber li.current, .kscard li.current, .sheetItem.current, .sheet-item.current, ' + '.rightPart .current, #rightFixed .current, .topicNumber_list li[class*="current"], .topicNumber_list li.cur, ' + '.fenye li.cur, .right_part li.cur, .sheetBox li.current, .topic-list li.current, ' + '[class*="card"] li.current, [class*="Card"] li.cur, [class*="card"] li.active, ' + '.sheet-item.current, .sheetItem.cur, .answerSheet li.current, .answerSheet li.cur'; for (const doc of collectAccessibleDocs()) { const cur = doc.querySelector(curSel); if (cur) { const t = (cur.getAttribute('data') || cur.getAttribute('data-index') || cur.textContent || '').trim(); const n = parseInt(t, 10); if (!isNaN(n) && n > 0) return n; } const lists = [ doc.querySelectorAll('.topicNumber_list li'), doc.querySelectorAll('.NumberCardUl li'), doc.querySelectorAll('.cardUl li'), doc.querySelectorAll('#cardUl li'), doc.querySelectorAll('.fenye li, .right_part li, .sheetBox li, .topic-list li, .answerSheet li'), doc.querySelectorAll('[class*="card"] li, [class*="Card"] li') ]; for (const list of lists) { if (!list.length) continue; const idx = Array.from(list).findIndex(li => li.classList.contains('current') || li.classList.contains('active') || li.classList.contains('cur') || /current|active/.test(li.className) ); if (idx >= 0) return idx + 1; } } return fallback; } function cxCollect(item, index, scene) { if (!isChapterMultiQuiz()) syncTopicCardForItem(item); scene = scene || GLOBAL._cxScene || getCxQuizScene(); const h3 = item.querySelector('h3.mark_name, h3'); const questionEl = h3 || item.querySelector('.mark_name, .Zy_TItle .clearfix, .Cy_TItle .clearfix, .question-title, h3 div, .topic-txt'); let options = []; let optionNodes = []; const collected = cxCollectOptionsForScene(item, scene); if (collected && collected.optionNodes.length) { options = collected.options; optionNodes = collected.optionNodes; } let type = cxResolveType(item, questionEl, optionNodes, index); if (type === 3 && options.length === 0) options.push('对', '错'); if (type === 14 || type === 15) { const containerSelector = type === 14 ? '.filling_answer' : '.reading_answer'; const compositeOpts = item.querySelectorAll(containerSelector + ' span.saveSingleSelect'); if (compositeOpts.length) { options = []; optionNodes = []; compositeOpts.forEach(el => { const t = cxOptionText(el); if (!t) return; options.push(t); optionNodes.push(el); }); } } let inputs = collectBlankInputs(item); if (!inputs.length) inputs = Array.from(item.querySelectorAll('textarea, input[type=text]')); const question = cxExtractQuestionText(questionEl, item); const qNo = cxQuestionNo(questionEl, index != null ? index + 1 : 1, item); return { question, options, optionNodes, type, inputs, $item: item, qNo, cxScene: scene }; } function cxFillEssay(type, answer, el) { if (!(type === 4 || type === 2 || type === 5) || !unsafeWindow.UE) return; const name = el.name || (el.querySelector && el.querySelector('textarea') && el.querySelector('textarea').name); if (!name) return; try { unsafeWindow.UE.getEditor(name).setContent(answer); if (GLOBAL._cxScene === 'answerBg' && GLOBAL._typeInputIndex === 0) { const saveBtn = document.getElementById(name.replace('answerEditor', 'save_')); if (saveBtn) { fireClick(saveBtn); try { saveBtn.click(); } catch (e) {} } } } catch (e) {} } // 清洗题干:去掉题号、题型标签、分值、末尾空括号/下划线等噪声 function cxCleanQuestion(q) { q = formatString(q); if (!q) return ''; q = q.replace(/[/g, '[').replace(/]/g, ']'); const typeWords = Object.keys(TYPE) .filter(k => /[\u4e00-\u9fff]/.test(k)) .sort((a, b) => b.length - a.length); const typePat = typeWords.join('|'); const stripOnce = () => { let s = q; s = s.replace(/^\s*第\s*\d+\s*题\s*[、..::)]?\s*/, ''); s = s.replace(/^\s*\d{1,3}\s*(?:[.、..。、::)]\s*|\s+)/, ''); s = s.replace(/^\s*\d{1,3}(?=【|\[)/, ''); s = s.replace(/^\s*[Qq]\s*\d+\s*[.、.::)]?\s*/, ''); s = s.replace(/^\s*[((]\s*\d+\s*[))]\s*/, ''); s = s.replace(/^\s*[一二三四五六七八九十百千]+[、..。.]\s*/, ''); s = s.replace(/^\s*【[^】]{0,48}】\s*/, ''); s = s.replace(/^\s*\[[^\]]{0,48}\]\s*/, ''); s = s.replace(/^\s*\([^()]{0,48}?(?:题|分|选|判断|填空|单选|多选)[^()]{0,24}?\)\s*/, ''); if (typePat) s = s.replace(new RegExp('^\\s*(?:' + typePat + ')\\s*[、,,..::)]?\\s*'), ''); s = s.replace(/^\s*(?:单选题|多选题|判断题|填空题|简答题|论述题|名词解释|主观题|问答题|单项选择|多项选择|单项选择题|多项选择题|对错题)\s*[、,,..::)]?\s*/i, ''); return s; }; let prev = ''; let guard = 0; while (q !== prev && guard++ < 12) { prev = q; q = stripOnce(); } q = q.replace(/[((]\s*[\d.]+\s*分\s*[))]/g, ''); q = q.replace(/[((]\s*本题\s*[\d.]+\s*分\s*[))]/g, ''); q = q.replace(/[((]\s*共\s*[\d.]+\s*分\s*[))]/g, ''); q = q.replace(/(?:^|\s)[((]\s*[))](?=\s|$)/g, ' '); q = q.replace(/[((]\s*[))]\s*$/g, ''); q = q.replace(/[__—\-]{2,}\s*$/g, ''); q = q.replace(/\s+/g, ' ').trim(); return q; } function cxExtractQuestionText(questionEl, item) { if (!questionEl) return ''; const dropSel = '.colorShallow, .colorDeep, .qz-number, .qzNum, .num-sort, .mark_tag, .tiType, .questionType, .score, .scoreNum, .tiMuType, .typeText, .qType, span[class*="type"], span[class*="Type"]'; const readNode = (node) => { if (!node) return ''; const clone = node.cloneNode(true); clone.querySelectorAll(dropSel).forEach(el => el.remove()); return filterImgText(clone); }; let text = readNode(questionEl); if (item) { const h3 = item.querySelector('h3.mark_name, h3'); if (h3) { const ht = readNode(h3); if (ht.length > text.length) text = ht; } const title = item.querySelector('.mark_name, .Zy_TItle .clearfix, .Cy_TItle .clearfix'); if (title && title !== questionEl) { const tt = readNode(title); if (tt.length > text.length) text = tt; } } if (!text) text = filterImgText(questionEl); return cxCleanQuestion(text); } function getQuizSessionKey(href) { href = href || location.href; try { const u = new URL(href); const parts = [u.pathname]; ['knowledgeid', 'chapterId', 'workId', 'courseId', 'classId', 'examId', 'taskrefId', 'jobid', 'cpi', 'moocId'].forEach(k => { const v = u.searchParams.get(k); if (v) parts.push(k + '=' + v); }); if (parts.length === 1 && u.search) parts.push(u.search); return parts.join('|'); } catch (e) { return (href.split('#')[0] || href); } } function initPanelSessionWatch() { let lastKey = ''; const bumpSession = () => { try { GM_setValue('bookwk_session_gen', Date.now()); } catch (e) {} }; const tick = () => { let href = location.href; try { if (window.top && window.top.location) href = window.top.location.href; } catch (e) {} const key = getQuizSessionKey(href); if (lastKey && key !== lastKey) { panel.clearRecords(); try { if (window.top === window.self) bumpSession(); } catch (e) { bumpSession(); } rematchWorkers(); } lastKey = key; }; tick(); setInterval(tick, 1500); } function initWorkerSessionSync() { GLOBAL._sessionGen = Number(GM_getValue('bookwk_session_gen', 0)) || 0; setInterval(() => { let gen = 0; try { gen = Number(GM_getValue('bookwk_session_gen', 0)) || 0; } catch (e) {} if (gen > GLOBAL._sessionGen) { GLOBAL._sessionGen = gen; rematchWorkers(); } }, 800); } function zhsCollect(item) { const questionEl = item.querySelector('.subject_describe, .questionContent, .questionName .centent-pre, .stem, h6 .processing_img, .question-title'); const optionNodes = Array.from(item.querySelectorAll('.subject_node .nodeLab, .optionUl label, .radio-view li, .checkbox-views label, .questiono-main label, label')); const options = optionNodes.map(el => filterImgText(el).replace(/^[A-Za-z][.、\s]+/, '').trim()).filter(Boolean); const typeEl = item.querySelector('.subject_type span, .questionTit, .letterSortNum, .title, .question-type-tag, .tip'); let type = getQuestionType(typeEl ? typeEl.textContent : ''); if (type === undefined) { const radios = item.querySelectorAll('input[type=radio], .el-radio__input'); const checks = item.querySelectorAll('input[type=checkbox], .el-checkbox__input'); type = radios.length === 2 ? 3 : radios.length > 2 ? 0 : checks.length > 2 ? 1 : 4; } if (type === 3 && options.length === 0) options.push('对', '错'); let inputs = collectBlankInputs(item); if (!inputs.length) inputs = Array.from(item.querySelectorAll('textarea, .question-detail-solution-textarea, input[type=text]')); return { question: cxCleanQuestion(filterImgText(questionEl)), options, optionNodes, type, inputs, $item: item }; } // 顶层窗口每次整页加载时重置暂停标志,避免上次遗留的暂停状态影响本次(iframe 不重置,保证本页内跨框架共享) try { if (window.top === window.self) setPaused(false); } catch (e) {} // ========== 全局功能:字体解密 + 视频挂机(所有匹配页面/子框架都会运行) ========== initFontDecrypt(); initMediaAuto(); // 智慧树前台播放 initCxSafetyMonitor(); // 学习通人脸识别暂停 initChaoxingCourse(); // 学习通后台模拟上报挂机 initPanelSessionWatch(); // 切换章节/作业时自动清空做题日志 initWorkerSessionSync(); // 跨 iframe 同步 worker 重匹配 // ========== 学习通 ========== function hasChapterQuizDoc() { for (const doc of collectAccessibleDocs()) { if (doc.querySelector('.TiMu, .clearfix .TiMu')) return true; } return false; } function isPhoneQuizPage() { const path = location.pathname + location.href; if (/\/phone\/|\/m\/|phoneWork|mobilework|mwork/i.test(path)) return true; return !!document.querySelector('.Py-mian, #Py-mian, .phone-work, .phoneWork, .mwork, .phone_content'); } runWorker({ name: '学习通手机端测验', cxScene: 'chapter', allowAutoSubmit: true, match: () => { if (!isPhoneQuizPage()) return false; return $$('.TiMu, .Py-mian .topic, .Py-mian .clearfix, #Py-mian .topic').length > 0; }, root: '.TiMu, .Py-mian .topic, .Py-mian .clearfix, #Py-mian .topic', collect: (item, index) => cxCollect(item, index, 'chapter'), onRoundDone: signalWorkQuizDone, wrap(data) { if (data.type === 6) data.type = 4; }, fill: cxFillEssay }); runWorker({ name: '学习通章节测验', cxScene: 'chapter', allowAutoSubmit: true, match: () => { if (/\/work\/doHomeWorkNew/.test(location.pathname) || /\/mooc-ans\/work\/doHomeWorkNew/.test(location.pathname)) return true; return /\/knowledge\/cards/.test(location.pathname) && hasChapterQuizDoc(); }, root: '.TiMu, .clearfix .TiMu', collect: (item, index) => cxCollect(item, index, 'chapter'), onRoundDone: signalWorkQuizDone, finished: async () => { for (const doc of collectAccessibleDocs()) { const card = doc.querySelector('.answerCard_list ul li.active, .answerCard_list1 li.active') || doc.querySelector('.answerCard_list ul li, .answerCard_list1 li'); if (card) { fireClick(card); try { card.click(); } catch (e) {} await sleep(400); } const btn = doc.querySelector('div.examPaper_box > div.switch-btn-box > button:nth-child(2)') || Array.from(doc.querySelectorAll('a, button, .jb_btn, .nextDiv .jb_btn, .nextDiv a')) .find(b => /下一题|下一页/.test((b.textContent || b.value || '').trim())); if (btn) { fireClick(btn); try { btn.click(); } catch (e) {} await sleep(1500); refreshQuizDoc(); try { decryptCxFont(); } catch (e) {} return true; } } const fw = getChapterQuizFrameWindow(); if (fw && typeof fw.getTheNextQuestion === 'function') { try { fw.getTheNextQuestion(1); await sleep(1500); return true; } catch (e) {} } return false; }, wrap(data) { if (data.type === 6) data.type = 4; }, fill: cxFillEssay }); runWorker({ name: '学习通作业', cxScene: 'answerBg', allowAutoSubmit: true, match: () => /\/mooc2\/work\/dowork/.test(location.pathname) || /\/mooc-ans\/mooc2\/work\/dowork/.test(location.pathname), root: '.questionLi', collect: (item, index) => cxCollect(item, index, 'answerBg'), onRoundDone: signalWorkQuizDone, fill: cxFillEssay }); runWorker({ name: '学习通新版考试', cxScene: 'answerBg', allowAutoSubmit: false, match: () => { const path = location.pathname; const href = location.href; const qDoc = resolveQuizDoc(); const single = (/reVersionTestStartNew/.test(path) || /\/exam-ans\/exam\/test/.test(path) || /\/exam\/test\//.test(path)) && (href.includes('newMooc=true') || !!qDoc.querySelector('.questionLi')); const all = /\/mooc2\/exam\/preview/.test(path) || /\/exam-ans\/mooc2\/exam\/preview/.test(path) || /\/mooc-ans\/mooc2\/exam\/preview/.test(path); return single || all; }, hook() { GLOBAL._qDoc = resolveQuizDoc(); GLOBAL._typeInputIndex = (location.href.includes('newMooc=true') && /reVersionTestStartNew/.test(location.pathname)) ? 1 : 0; GLOBAL._cxScene = 'answerBg'; }, root: '.questionLi', collect: (item, index) => cxCollect(item, index, 'answerBg'), wrap(data) { if (data.type === 6) data.type = 4; }, finished: async () => { for (const doc of collectAccessibleDocs()) { const btn = Array.from(doc.querySelectorAll('a, button, .jb_btn, .nextDiv .jb_btn, .nextDiv a')) .find(b => /下一题/.test((b.textContent || b.value || '').trim())); if (btn) { fireClick(btn); await sleep(1500); refreshQuizDoc(); try { decryptCxFont(); } catch (e) {} return true; } } return false; }, fill: cxFillEssay }); runWorker({ name: '学习通旧版考试', cxScene: 'oldExam', allowAutoSubmit: false, match: () => /\/exam\/test\/reVersionTestStartNew/.test(location.pathname) && !location.href.includes('newMooc=true'), root: '.TiMu', collect: (item, index) => cxCollect(item, index, 'oldExam'), finished: async () => { const btn = document.querySelector('.saveYl'); if (btn && /下一题/.test(btn.textContent) && unsafeWindow.getTheNextQuestion) { try { unsafeWindow.getTheNextQuestion(1); await sleep(1500); return true; } catch (e) {} } return false; }, fill: cxFillEssay }); runWorker({ name: '学习通随堂测验', cxScene: 'inclass', allowAutoSubmit: true, match: () => /\/page\/quiz\/stu\/answerQuestion/.test(location.pathname), root: '.question-item', collect: (item, index) => cxCollect(item, index, 'inclass'), onRoundDone: signalWorkQuizDone, wrap(data) { if (data.type === 16) data.type = 3; }, fill(type, answer, el) { if (type === 4 || type === 2) { const inp = el.tagName === 'INPUT' ? el : el.querySelector && el.querySelector('input, textarea'); if (inp) { inp.value = answer; inp.dispatchEvent(new Event('input', { bubbles: true })); inp.dispatchEvent(new Event('change', { bubbles: true })); } } } }); // ========== 智慧树 ========== runWorker({ name: '智慧树作业考试', allowAutoSubmit: () => location.href.includes('/dohomework/') && !location.href.includes('/doexamination/'), match: () => location.host.includes('zhihuishu') && ( location.pathname === '/stuExamWeb.html' || location.href.includes('/webExamList/dohomework/') || location.href.includes('/webExamList/doexamination/') ), root: '.examPaper_subject', intv: () => document.querySelector('.answerCard') || document.querySelector('.subject_type'), collect: zhsCollect, wrap(data) { checkSafetyPauses(); }, fill(type, answer, el) { if ((type === 4 || type === 2) && unsafeWindow.UE && el.name) { try { unsafeWindow.UE.getEditor(el.name).setContent(answer); } catch (e) {} } }, fillFinish(result, data) { const qNo = (data && data.qNo != null) ? data.qNo : (GLOBAL.index + 1); const card = document.querySelectorAll('.answerCard_list li, .answerCard_list1 li'); const cardIdx = qNo - 1; if (card[cardIdx]) { fireClick(card[cardIdx]); try { card[cardIdx].click(); } catch (e) {} } const next = Array.from(document.querySelectorAll('.el-button')).find(b => /下一题/.test(b.textContent)); if (next) next.click(); } }); runWorker({ name: '智慧树学分课作业', allowAutoSubmit: true, match: () => location.host.includes('zhihuishu') && ( location.href.includes('/atHomeworkExam/stu/homeworkQ/exerciseList') || location.href.includes('/atHomeworkExam/stu/examQ/examexercise') ), root: '.questionBox', intv: () => document.querySelector('.answerCard') || document.querySelector('.questionTit'), collect: zhsCollect, finished: async () => { const next = document.querySelector('.Topicswitchingbtn:not(.Topicswitchingbtn-gray)'); if (next && /下一题/.test(next.textContent)) { next.click(); await sleep(1200); return true; } return false; } }); runWorker({ name: '智慧树学分课考试H5', allowAutoSubmit: false, match: () => location.host === 'studentexambaseh5.zhihuishu.com', root: '.ques-detail', intv: () => document.querySelector('.questionContent'), collect: zhsCollect, finished: async () => { const next = document.querySelector('.next-topic'); if (next && !next.classList.contains('noNext')) { next.click(); await sleep(1200); return true; } return false; } }); runWorker({ name: '智慧树单题模式', allowAutoSubmit: true, match: () => location.host === 'www.zjooc.cn' && location.pathname.includes('/singleQuestion/do/'), root: '.question_content', intv: () => document.querySelector('.question_title'), collect: zhsCollect, finished: async () => { const next = Array.from(document.querySelectorAll('.el-button')).find(b => /下一题/.test(b.textContent) && !b.classList.contains('is-disabled')); if (next) { next.click(); await sleep(1200); return true; } return false; } }); })();