// ==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 = `
| # | 题目 | 答案 |
|---|