// ==UserScript== // @name WE Learn 学习辅助 // @namespace https://cxbank.icu/welearn-pub // @version 1.0.0 // @description 辅助填写 WE Learn 课件中的练习题,自动提交与翻页。需要在支持 iframe 注入的脚本管理器中使用(推荐 Tampermonkey)。 // @author ZZ // @antifeature tracking // @match *://welearn.sflep.com/* // @match *://centercourseware.sflep.com/* // @match *://course.sflep.com/* // @match *://wetest.sflep.com/* // @grant none // @run-at document-idle // @noframes false // @license MIT // ==/UserScript== (function () { 'use strict'; /* ========================================================= * 两个窗口分工(照开源项目):顶层管翻页,iframe 管答题 * welearn.sflep.com 和 centercourseware.sflep.com 不同源, * iframe 里调 top.NextSCO() 会被跨域拦死 —— 所以翻页必须在顶层做。 * ========================================================= */ var HOST = location.hostname || ''; var IS_CRS = HOST.indexOf('centercourseware') >= 0; var IS_TOP = !IS_CRS && /(^|\.)sflep\.com$/.test(HOST); var BUILD = '1.5.9'; if (IS_TOP) { /* 顶层徽标 + 与 iframe 握手:能一眼看出课件里到底有没有注入成功 */ var helloAt = 0, helloVer = '', startedAt = Date.now(); (function topBadge() { var badge = null; function paint() { if (!badge) return; var hasFrame = false; try { hasFrame = !!document.querySelector('iframe'); } catch (e) {} var txt = 'WE Learn 助手 ' + BUILD + ' · '; if (helloAt) txt += '课件已连接 (' + helloVer + ')'; else if (!hasFrame) txt += '请先点开一节课件'; else if (Date.now() - startedAt > 6000) txt += '课件加载中,稍等一下'; else txt += '正在连接课件…'; badge.textContent = txt; badge.style.background = helloAt ? 'rgba(52,199,89,.94)' : (!hasFrame ? 'rgba(142,142,147,.94)' : (Date.now() - startedAt > 6000 ? 'rgba(255,149,0,.94)' : 'rgba(142,142,147,.94)')); } function add() { if (!document.body) { setTimeout(add, 500); return; } if (badge) return; badge = document.createElement('div'); badge.id = 'wl-top-badge'; badge.style.cssText = 'position:fixed;right:12px;bottom:12px;z-index:2147483000;' + 'padding:7px 12px;border-radius:99px;background:rgba(142,142,147,.94);color:#fff;' + 'font:600 11.5px -apple-system,"Segoe UI",sans-serif;box-shadow:0 4px 14px rgba(0,0,0,.2);' + 'user-select:none;cursor:help'; badge.title = '打开一节课件后会自动连接'; badge.style.pointerEvents = 'auto'; badge.onclick = function () { try { badge.title = '有问题请看面板日志'; } catch (e) {} }; document.body.appendChild(badge); paint(); setInterval(paint, 1500); /* 主动问子框架「你在吗」,避免对面先发消息造成误判 */ [3000, 8000, 15000, 30000].forEach(function (ms) { setTimeout(function () { try { for (var i = 0; i < window.frames.length; i++) { window.frames[i].postMessage({ __wl: 1, cmd: 'who' }, '*'); } } catch (e) {} }, ms); }); setTimeout(function () { try { badge.style.transition = 'opacity 1s'; badge.style.opacity = '.5'; } catch (e) {} }, 12000); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', add); else setTimeout(add, 300); })(); window.addEventListener('message', function (ev) { var d = ev && ev.data; if (!d || d.__wl !== 1) return; if (d.cmd === 'course') { var cname = ''; try { var t1 = document.querySelector('.course-name,.current-course,.crumb,.path-nav'); cname = t1 ? String(t1.textContent || '').trim() : ''; if (!cname) cname = String(document.title || '').trim(); if (!cname) { var h = document.querySelector('h1,h2,h3'); cname = h ? String(h.textContent || '').trim() : ''; } } catch (e) {} try { ev.source.postMessage({ __wl: 2, cmd: 'courseName', name: cname.slice(0, 80) }, '*'); } catch (e) {} } if (d.cmd === 'hello') { helloAt = Date.now(); helloVer = d.ver || ''; } if (d.cmd === 'who') { /* iframe 在问「顶层在吗」,回它一声 */ try { ev.source.postMessage({ __wl: 2, cmd: 'helloAck' }, '*'); } catch (e) {} } /* 顶层代传诊断:iframe 里 fetch 被拦时走这里 */ if (d.cmd === 'diag' && d.payload) { fetch('https://api.cxbank.icu/diag/upload', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(d.payload), }).then(function (r) { return r.json(); }) .then(function (j) { try { ev.source.postMessage({ __wl: 2, cmd: 'diagResult', tag: d.tag, result: j }, '*'); } catch (e) {} }) .catch(function (e) { try { ev.source.postMessage({ __wl: 2, cmd: 'diagResult', tag: d.tag, result: { ok: false, reason: e.message } }, '*'); } catch (x) {} }); } }); function advanceTop() { /* 1. 平台自己的全局函数最可靠 */ try { if (typeof window.NextSCO === 'function') { window.NextSCO(); return 'NextSCO'; } } catch (e) {} /* 2. 目录里点"当前节"的下一个 */ try { var all = Array.prototype.slice.call(document.querySelectorAll('a[onclick^="SelectSCO"]')); var cur = document.querySelector('li.courseware_current a'); var i = all.indexOf(cur); if (i >= 0 && all[i + 1]) { all[i + 1].click(); return 'next-sco'; } if (i >= 0 && i === all.length - 1 && all.length > 1) { all[1].click(); return 'loop'; } if (all.length) { all[all.length - 1].click(); return 'last-sco'; } } catch (e) {} return ''; } window.addEventListener('message', function (ev) { var d = ev && ev.data; if (!d || d.__wl !== 1) return; if (d.cmd === 'next') { var how = advanceTop(); try { console.log('[WE Learn] 顶层翻页:', how || '失败'); } catch (e) {} try { ev.source.postMessage({ __wl: 2, cmd: 'advanced', how: how }, '*'); } catch (e) {} } }); return; } if (!IS_CRS) return; var LS = { autoFill: 'wl_autofill', autoSubmit: 'wl_autosubmit', autoNext: 'wl_autonext', intervalSec: 'wl_interval_sec', interval: 'wl_interval', /* 旧版存的是"分钟",只读来迁移 */ running: 'wl_running', lic: 'wl_lic', /* { code, exp } */ }; /* ========================================================= * 授权:小程序看完广告领激活码,填进来才能用 * ========================================================= */ var LIC_API = 'https://api.cxbank.icu/wl/reward/verify'; var LIC = { code: '', exp: 0, checking: false, free: false, online: 0 }; function loadLic() { try { var j = JSON.parse(localStorage.getItem(LS.lic) || '{}'); LIC.code = String(j.code || ''); LIC.exp = Number(j.exp) || 0; } catch (e) { LIC.code = ''; LIC.exp = 0; } } function saveLic(code, exp) { LIC.code = code; LIC.exp = exp; try { localStorage.setItem(LS.lic, JSON.stringify({ code: code, exp: exp })); } catch (e) {} } function licActive() { return true; } function licLeftText() { if (LIC.free) return '内测放行中'; if (!licActive()) return '未激活'; var h = Math.max(0, Math.round((LIC.exp - Date.now()) / 3600e3)); if (h >= 48) return '已激活,剩 ' + Math.round(h / 24) + ' 天'; if (h >= 1) return '已激活,剩 ' + h + ' 小时'; return '已激活,剩 ' + Math.max(1, Math.round((LIC.exp - Date.now()) / 60e3)) + ' 分钟'; } /* 向顶层要课程名(跨域只能走消息) */ function askCourseName() { return new Promise(function (resolve) { if (!window.parent || window.parent === window) { resolve(''); return; } var done = false; function onMsg(ev) { var d = ev && ev.data; if (!d || d.__wl !== 2 || d.cmd !== 'courseName') return; done = true; window.removeEventListener('message', onMsg); resolve(String(d.name || '')); } window.addEventListener('message', onMsg); try { window.parent.postMessage({ __wl: 1, cmd: 'course' }, '*'); } catch (e) { resolve(''); return; } setTimeout(function () { if (!done) { window.removeEventListener('message', onMsg); resolve(''); } }, 3000); }); } /* 服务端总开关:内测放行时跳过激活码 */ async function checkBypass() { try { var r = await fetch('https://api.cxbank.icu/wl/lic/status?_=' + Date.now(), { cache: 'no-store', signal: (function () { try { return AbortSignal.timeout(8000); } catch (e) { return undefined; } })(), }); var j = await r.json(); var on = !!(j && j.bypass); if (j && typeof j.online === 'number') { LIC.online = j.online; try { renderStatus(); } catch (e) {} } if (on !== LIC.free) { LIC.free = on; logInfo(on ? '内测放行已开启:无需激活码' : '内测放行已关闭:需要激活码'); renderLic(); } } catch (e) {} } async function activate(code) { var c = String(code || '').trim().toUpperCase(); if (!c) { logWarn('请先填入激活码'); return false; } LIC.checking = true; try { var r = await fetch(LIC_API, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ code: c }), signal: (function () { try { return AbortSignal.timeout(12000); } catch (e) { return undefined; } })(), }); var j = await r.json(); if (j && j.ok) { saveLic(j.code || c, Number(j.exp) || 0); logOk('激活成功:' + licLeftText()); renderLic(); return true; } logWarn('激活失败:' + ((j && j.reason) || '未知')); return false; } catch (e) { logWarn('激活请求失败:' + e.message); return false; } finally { LIC.checking = false; } } /* 翻页间隔统一用【秒】存:自定义可以精确到 5 秒 */ function loadIntervalSec() { var s = Number(localStorage.getItem(LS.intervalSec)); if (isFinite(s) && s > 0) return Math.round(s); var m = Number(localStorage.getItem(LS.interval)); if (isFinite(m) && m > 0) return Math.round(m * 60); return 360; } var CFG = { autoFill: localStorage.getItem(LS.autoFill) !== '0', autoSubmit: localStorage.getItem(LS.autoSubmit) !== '0', autoNext: localStorage.getItem(LS.autoNext) !== '0', intervalSec: loadIntervalSec(), // 秒 tickMs: 1000, }; var S = { course: '', running: false, phase: '待机', nextAt: 0, filled: 0, submitted: 0, pages: 0, timer: null, pageKey: '', log: [], /* 卡住检测 */ nextFail: 0, emptyPages: 0, diagAt: 0, busy: false, pendingSubmit: false, }; var DIAG_URL = 'https://api.cxbank.icu/diag/upload'; var UPDATE_URL = 'https://api.cxbank.icu/welearn.user.js'; var NEW_VER = ''; function cmpVer(a, b) { var x = String(a || '0').split('.').map(Number); var y = String(b || '0').split('.').map(Number); for (var i = 0; i < Math.max(x.length, y.length); i++) { var p = x[i] || 0, q = y[i] || 0; if (p !== q) return p > q ? 1 : -1; } return 0; } async function checkUpdate() { try { var r = await fetch('https://api.cxbank.icu/welearn/info?_=' + Date.now(), { cache: 'no-store', signal: (function () { try { return AbortSignal.timeout(8000); } catch (e) { return undefined; } })(), }); var j = await r.json(); var v = String((j && j.version) || ''); if (!v || cmpVer(v, BUILD) <= 0) return; NEW_VER = v; logWarn('发现新版本 ' + v + '(当前 ' + BUILD + ')'); renderUpdate(); } catch (e) {} } function renderUpdate() { try { var box = el('upd'); if (!box || !NEW_VER) return; box.style.display = 'block'; box.innerHTML = ''; var b = document.createElement('button'); b.className = 'upd-btn'; b.textContent = '有新版本 ' + NEW_VER + ',点这里更新'; b.onclick = function () { try { window.open(UPDATE_URL, '_blank'); } catch (e) {} logInfo('已打开安装地址,确认安装后刷新页面'); }; box.appendChild(b); } catch (e) {} } /* ---------------- 小工具 ---------------- */ function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); } function vw(el) { if (!el) return false; try { var r = el.getBoundingClientRect(); if (!r.width || !r.height) return false; var st = getComputedStyle(el); return st.display !== 'none' && st.visibility !== 'hidden' && Number(st.opacity) !== 0; } catch (e) { return false; } } function qs(sel, root) { try { return (root || document).querySelector(sel); } catch (e) { return null; } } function qsa(sel, root) { try { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); } catch (e) { return []; } } /* 让框架认账的输入(jQuery + 原生事件都发一遍) */ function setInput(el, val) { try { el.removeAttribute('readonly'); } catch (e) {} try { el.focus(); } catch (e) {} var setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value'); try { if (setter && setter.set) setter.set.call(el, val); else el.value = val; } catch (e) { try { el.value = val; } catch (e2) {} } ['input', 'change', 'keyup', 'blur'].forEach(function (t) { try { el.dispatchEvent(new Event(t, { bubbles: true })); } catch (e) {} }); try { if (window.jQuery) { window.jQuery(el).trigger('input').trigger('change').trigger('keyup').trigger('blur'); } } catch (e) {} } function logInfo(t) { addLog('info', t); } function logOk(t) { addLog('ok', t); } function logWarn(t) { addLog('warn', t); } function addLog(kind, t) { var t0 = new Date().toLocaleTimeString('zh-CN', { hour12: false }); S.log.push({ kind: kind, text: t, time: t0 }); if (S.log.length > 120) S.log.shift(); try { renderLog(); } catch (e) {} try { console.log('[WE Learn]', t); } catch (e) {} } /* ========================================================= * 诊断包:卡住时点一下,把现场发到服务器 * ========================================================= */ function domCount(sel) { try { return document.querySelectorAll(sel).length; } catch (e) { return -1; } } function collectDiag() { var L = []; var P = function (s) { L.push(s); }; P('=== WE Learn 助手诊断 ==='); P('版本: ' + BUILD); P('时间: ' + new Date().toLocaleString('zh-CN')); P('页面: ' + location.href); P('主机: ' + location.hostname + ' 在 iframe 里: ' + (window.top !== window.self)); P('UA: ' + navigator.userAgent); P('视口: ' + window.innerWidth + 'x' + window.innerHeight + ' ready: ' + document.readyState); P(''); P('=== 运行状态 ==='); P('运行中: ' + S.running + ' 阶段: ' + S.phase); P('已填/已交/页数: ' + S.filled + '/' + S.submitted + '/' + S.pages); P('翻页失败次数: ' + S.nextFail + ' 连续空页: ' + S.emptyPages); P('pageKey: ' + String(S.pageKey).slice(0, 160)); P('下次翻页: ' + (S.nextAt ? Math.max(0, Math.round((S.nextAt - Date.now()) / 1000)) + ' 秒后' : '-')); P(''); P('=== 设置 ==='); P('自动填=' + CFG.autoFill + ' 自动交=' + CFG.autoSubmit + ' 自动翻页=' + CFG.autoNext + ' 间隔=' + fmtSec(CFG.intervalSec)); P(''); P('=== DOM 计数 ==='); [ '[data-controltype="pagecontrol"]', '[data-controltype="page"]', '[data-controltype="group"]', '[data-controltype="choice"]', '[data-controltype="filling"]', '[data-controltype="submit"]', '[data-controltype="reset"]', '[data-id="nextcmd"]', '.layui-layer', 'audio', 'input[data-solution]', 'li[data-solution]', ].forEach(function (s) { P(' ' + s + ' → ' + domCount(s)); }); P(''); P('=== 读到的题目 ==='); try { var qsList = readQuestions(document); P('题目数: ' + qsList.length); qsList.slice(0, 12).forEach(function (q, i) { if (q.kind === 'choice') { P(' [' + (i + 1) + '] choice 选项' + q.options + ' 正确' + q.picks.length + ' | ' + q.stem); } else { P(' [' + (i + 1) + '] filling 答案="' + String(q.answer).slice(0, 50) + '"'); } }); } catch (e) { P(' 读取异常: ' + e.message); } P(''); P('=== 关键元素可见性 ==='); try { var sub = qsa('[data-controltype="submit"]').filter(vw)[0]; P(' 提交按钮可见: ' + (!!sub) + (sub ? ' 文本=' + (sub.textContent || '').trim().slice(0, 20) : '')); var nx = qs('[data-id="nextcmd"]'); P(' nextcmd 可见: ' + vw(nx)); var pages = qsa('[data-controltype="page"]'); P(' 页面项总数: ' + pages.length); var cur = pages.filter(function (p) { try { return p.querySelector('[data-controltype="group"]') && vw(p); } catch (e) { return false; } })[0]; P(' 当前页: ' + (cur ? (cur.getAttribute('data-scoid') || '?') + ' / ' + (cur.getAttribute('data-title') || '') : '识别不到')); P(' 提交状态文字: ' + (submitInfoText() || '(无)').slice(0, 60)); } catch (e) { P(' 异常: ' + e.message); } P(''); P('=== 分页(Role-Play 等)==='); try { var tabs = partTabs(); P(' 分页数: ' + tabs.length); tabs.forEach(function (tb, i) { var isCur = /cur|active/.test(String(tb.className || '')); var fill = qsa('[data-controltype="filling"]', tb).length; var done = qsa('[data-itemtype="myresult"]', tb).filter(function (m) { var t = String(m.textContent || '').trim(); return t && t !== 'click here'; }).length; var choice = qsa('[data-controltype="choice"]', tb).length; P(' [' + (i + 1) + '] ' + String(tb.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 16) + (isCur ? ' *当前*' : '') + ' 填空' + fill + ' 已答' + done + ' 选择' + choice); }); } catch (e) { P(' 异常: ' + e.message); } P(''); P('=== 结构片段(第一个题块)==='); try { var box = qs('[data-controltype="choice"],[data-controltype="filling"]'); P(box ? box.outerHTML.replace(/\s+/g, ' ').slice(0, 1600) : '(没有题块)'); } catch (e) { P(' 异常: ' + e.message); } P(''); P('=== 层级 ==='); try { var chain = []; var n = document.body, d = 0; while (n && d < 6) { chain.push(n.tagName + (n.id ? '#' + n.id : '') + (n.className ? '.' + String(n.className).split(' ')[0] : '')); n = n.parentElement; d++; } P(' ' + chain.join(' < ')); P(' top 可访问: ' + (function () { try { return !!window.top.document; } catch (e) { return false; } })()); P(' top.URL: ' + (function () { try { return window.top.location.href; } catch (e) { return '(跨域不可读)'; } })()); } catch (e) {} P(''); P('=== 日志(最近 60 条)==='); S.log.slice(-60).forEach(function (x) { P('[' + x.time + '] ' + x.text); }); return L.join('\n'); } async function uploadDiag(reason) { try { var text = collectDiag(); logInfo('正在上传诊断(' + text.length + ' 字节)…'); var payload = { text: text, ver: BUILD, reason: reason || '手动上传', page: location.href.slice(0, 120) }; var j = null; /* ① 先自己传;iframe 里可能被拦,所以还有 ② */ try { var r = await fetch(DIAG_URL, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload), signal: (function () { try { return AbortSignal.timeout(15000); } catch (e) { return undefined; } })(), }); j = await r.json(); } catch (e) { logWarn('本页直传失败(' + e.message + '),改由顶层代传'); j = null; } /* ② 顶层窗口代传 */ if (!j || !j.ok) j = await uploadViaTop(payload); if (j && j.ok) { logOk('诊断已上传:' + j.id + (j.dup ? '(内容重复,沿用旧的)' : '')); S.diagAt = Date.now(); try { console.log('诊断地址: https://api.cxbank.icu/diag?id=' + j.id); } catch (e) {} } else { logWarn('诊断上传失败:' + ((j && j.reason) || '未知')); logWarn('可在控制台执行 __wl.copy() 复制诊断文本发我'); } return j; } catch (e) { logWarn('诊断上传异常:' + e.message); return null; } } /* 把诊断文本交给顶层窗口上传,等它回话 */ function uploadViaTop(payload) { return new Promise(function (resolve) { if (!window.parent || window.parent === window) { resolve(null); return; } var tag = 'diag_' + Date.now(); var finished = false; function onMsg(ev) { var d = ev && ev.data; if (!d || d.__wl !== 2 || d.cmd !== 'diagResult' || d.tag !== tag) return; finished = true; window.removeEventListener('message', onMsg); resolve(d.result || null); } window.addEventListener('message', onMsg); try { window.parent.postMessage({ __wl: 1, cmd: 'diag', tag: tag, payload: payload }, '*'); } catch (e) { resolve(null); return; } setTimeout(function () { if (finished) return; window.removeEventListener('message', onMsg); resolve(null); }, 20000); }); } /* 卡住自动上报:10 分钟最多一次 */ function autoDiag(reason) { if (Date.now() - S.diagAt < 10 * 60 * 1000) return; S.diagAt = Date.now(); uploadDiag(reason).catch(function () {}); } /* ========================================================= * 1. 读答案 —— 全部来自页面自带标记 * choice :
  • 正确项
  • * filling: * ========================================================= */ function readQuestions(scope) { var out = []; qsa('[data-controltype="choice"]', scope).forEach(function (box) { if (!vw(box)) return; var lis = qsa('ul[data-itemtype="options"] > li', box); var right = lis.filter(function (li) { return li.hasAttribute('data-solution'); }); var stem = (qs('[data-itemtype="stem"]', box) || {}).textContent || ''; out.push({ kind: 'choice', box: box, stem: stem.trim().slice(0, 60), picks: right, options: lis.length, }); }); qsa('[data-controltype="filling"]', scope).forEach(function (box) { if (!vw(box)) return; var input = qs('input[data-itemtype="input"][data-solution]', box); if (!input) return; out.push({ kind: 'filling', box: box, stem: (input.getAttribute('data-solution') || '').slice(0, 40), input: input, answer: input.getAttribute('data-solution') || '', }); }); return out; } /* ========================================================= * 2. 填答案 * ========================================================= */ async function fillCurrent() { var scope = document; var qsList = readQuestions(scope); if (!qsList.length) { logInfo('本页没有题目'); return 0; } var n = 0; for (var i = 0; i < qsList.length; i++) { var q = qsList[i]; try { if (q.kind === 'choice') { if (!q.picks.length) { logWarn('有一道选择题没标答案,跳过'); continue; } if (q.picks.length === 1) { var li = q.picks[0]; if (!/selected|active|checked/.test(li.className)) { li.click(); await sleep(180); n++; } } else { /* 多选 */ for (var k = 0; k < q.picks.length; k++) { q.picks[k].click(); await sleep(160); } n++; } } else if (q.kind === 'filling') { if (!q.answer) continue; var cur = String(q.input.value || '').trim(); var shown = String((qs('[data-itemtype="myresult"]', q.box) || {}).textContent || '').trim(); var already = (cur === q.answer) || (shown.indexOf(q.answer) >= 0 && shown !== 'click here'); if (already) continue; /* 只读输入框 = 词库题:必须走"点答题卡" (input 带 readonly / data-alwaysreadonly,赋值页面不认) */ var locked = q.input.hasAttribute('readonly') || q.input.hasAttribute('data-alwaysreadonly'); if (locked) { if (await pickFromAnswerSheet(q.answer, q.box)) { n++; trace && trace('fill', '答题卡选中 ' + q.answer.slice(0, 20)); continue; } logWarn('答题卡里没找到「' + q.answer.slice(0, 24) + '」'); } setInput(q.input, q.answer); await sleep(160); if (String(q.input.value || '').trim() !== q.answer) { if (await pickFromAnswerSheet(q.answer, q.box)) { n++; continue; } } n++; } } catch (e) { logWarn('填第 ' + (i + 1) + ' 题出错:' + e.message); } } S.filled += n; if (n) logOk('已填 ' + n + ' 题(共 ' + qsList.length + ' 题)'); return n; } /* Role-Play 这类题会分成好几个分页(Scenario 1/2/3…), 每页各有一批填空,必须全部答完才允许提交。 */ function partTabs() { return qsa('.unit_test_titbox > li'); } async function fillAllParts() { var tabs = partTabs(); if (tabs.length < 2) return await fillCurrent(); var total = 0; for (var i = 0; i < tabs.length; i++) { var tab = tabs[i]; var name = String((tab.textContent || '')).replace(/\s+/g, ' ').trim().slice(0, 14); try { (tab.querySelector('a') || tab).click(); } catch (e) {} await sleep(750); var n = await fillCurrent(); total += n; if (n) logInfo('分页 ' + (i + 1) + '/' + tabs.length + '(' + name + ')填了 ' + n + ' 题'); } /* 回到第一页,交回页面自己的判分逻辑 */ try { (tabs[0].querySelector('a') || tabs[0]).click(); } catch (e) {} await sleep(500); if (!total) logInfo('所有分页都没有需要新填的题'); return total; } /* ========================================================= * 3. 提交(会有 layer 确认框) * ========================================================= */ async function confirmLayer() { for (var i = 0; i < 20; i++) { var cands = qsa('.layui-layer-btn0, .layui-layer-btn a').filter(vw); for (var c = 0; c < cands.length; c++) { var t = String(cands[c].textContent || '').trim(); var cls = String(cands[c].className || ''); if (cls.indexOf('btn0') >= 0 || /确定|确认|是|OK|Yes|Submit/i.test(t)) { cands[c].click(); return true; } } await sleep(150); } return false; } function submitInfoText() { var info = qs('[data-controltype="submitinfo"]'); return info ? String(info.textContent || '').replace(/\s+/g, ' ').trim() : ''; } async function submitCurrent() { var btn = qsa('[data-controltype="submit"]').filter(vw).filter(function (b) { return /submit|提交/i.test(b.textContent || ''); })[0]; if (!btn) { logInfo('本页没有提交按钮(可能不是练习页)'); return false; } if (!btn.hasAttribute('data-id')) return false; var before = submitInfoText(); btn.click(); await sleep(350); var confirmed = await confirmLayer(); if (confirmed) logOk('已点提交并确认'); else logInfo('点过提交(没弹确认框)'); /* 校验是否真的提交了:页面底部那行「尚未提交。」会变 */ await sleep(900); var after = submitInfoText(); if (after && after === before && /尚未提交/.test(after)) { logWarn('页面仍显示未提交,再点一次'); btn.click(); await sleep(350); await confirmLayer(); await sleep(900); after = submitInfoText(); } if (after && !/尚未提交/.test(after)) { logOk('提交结果:' + after.slice(0, 40)); S.submitted++; S.pendingSubmit = false; } else { logWarn('提交没生效(可能是题目还没答完,页面不让交)'); S.pendingSubmit = true; } await sleep(500); return true; } /* ========================================================= * 4. 翻页:优先页面自己的 nextcmd * ========================================================= */ function nextPage() { var btn = qs('[data-id="nextcmd"]') || qs('.nextcmd'); if (btn && vw(btn)) { btn.click(); return true; } try { if (typeof window.NextSCO === 'function') { window.NextSCO(); return true; } } catch (e) {} /* 跨域调不到 top.NextSCO,就发消息让顶层自己翻(开源项目也是这么分工的) */ try { if (window.parent && window.parent !== window) { window.parent.postMessage({ __wl: 1, cmd: 'next' }, '*'); return true; } } catch (e) {} return false; } /* 词库/角色扮演题:点开「答题卡」(.AnswerSheet) 再点里面文字相同的 li。 这是 choosebox.js 里 fn_showChooseBox 的流程 —— 直接改 input.value 页面不认。 */ async function pickFromAnswerSheet(answer, box) { var want = String(answer || '').trim(); if (!want) return false; try { var trigger = qs('[data-itemtype="myresult"]', box) || qs('input', box); if (!trigger) return false; trigger.click(); await sleep(400); var sheets = qsa('.AnswerSheet').filter(vw); if (!sheets.length) return false; for (var s = 0; s < sheets.length; s++) { var lis = qsa('.AnswerSheet_content li', sheets[s]).filter(vw); for (var i = 0; i < lis.length; i++) { if (String(lis[i].textContent || '').trim() !== want) continue; lis[i].click(); await sleep(320); var shown = String((qs('[data-itemtype="myresult"]', box) || {}).textContent || '').trim(); var val = String((qs('input', box) || {}).value || '').trim(); if (shown.indexOf(want) >= 0 || val === want) return true; } } } catch (e) {} return false; } function pageSignature() { try { var t = qs('.divTopBar span') || qs('header span'); var cur = qs('[data-controltype="page"].active') || qs('[data-controltype="page"][style*="display: block"]'); return (t ? t.textContent : '') + '|' + (cur ? cur.getAttribute('data-scoid') : location.hash) + '|' + location.href; } catch (e) { return location.href; } } /* ========================================================= * 5. 主循环 * ========================================================= */ function fmtSec(sec) { sec = Math.round(Number(sec) || 0); if (sec < 60) return sec + ' 秒'; var m = Math.floor(sec / 60), s = sec % 60; return m + ' 分' + (s ? ' ' + s + ' 秒' : ''); } function nextDelayMs() { var sec = Math.max(10, Math.min(1740, Number(CFG.intervalSec) || 360)); /* 10s ~ 29min */ var jitter = 0.85 + Math.random() * 0.3; /* ±15% 抖动,别每次都一样 */ return sec * 1000 * jitter; } async function tick() { if (!S.running) return; /* 关键:tick 里有大量 await(填答案一页可能几十秒), 而 setInterval 每秒都会调一次 —— 不加锁会并发重入, 表现就是「还在填答案,另一条流程已经把页翻了」。 */ if (S.busy) return; S.busy = true; try { await tickBody(); } catch (e) { logWarn('处理异常:' + e.message); } finally { S.busy = false; } } async function tickBody() { var key = pageSignature(); if (key !== S.pageKey) { S.pageKey = key; S.pages++; S.nextAt = 0; logInfo('进入新页面(第 ' + S.pages + ' 页),先处理题目'); setPhase('处理中'); await sleep(800); var before = S.filled; if (CFG.autoFill) await fillAllParts(); if (CFG.autoSubmit) await submitCurrent(); /* 处理完才开始倒计时 —— 填答案花的时间不算进挂机时间 */ if (CFG.autoNext) { S.nextAt = Date.now() + nextDelayMs(); logInfo('处理完毕,' + Math.round((S.nextAt - Date.now()) / 1000) + ' 秒后翻页'); } /* 卡住检测:连续好几页一道题都读不到 → 自动上报 */ if (S.filled === before) { S.emptyPages++; if (S.emptyPages >= 3) { logWarn('连续 ' + S.emptyPages + ' 页没读到题目,已自动上传诊断'); autoDiag('连续没读到题目'); S.emptyPages = 0; } } else { S.emptyPages = 0; } setPhase(CFG.autoNext ? '挂机中' : '已填完'); } /* 有题目但没提交成功 → 先补交,别急着翻页 */ if (CFG.autoNext && S.nextAt && Date.now() >= S.nextAt) { if (S.pendingSubmit && CFG.autoSubmit) { S.pendingSubmit = false; setPhase('补交'); logInfo('上次没提交成功,翻页前再交一次'); await submitCurrent(); S.nextAt = Date.now() + 8000; /* 交完再等一会儿 */ renderStatus(); return; } S.nextAt = 0; setPhase('翻页'); var ok = nextPage(); if (ok) { S.nextFail = 0; logInfo('已翻到下一节'); } else { S.nextFail++; logWarn('翻页失败(第 ' + S.nextFail + ' 次),30 秒后重试'); S.nextAt = Date.now() + 30000; if (S.nextFail >= 3) { logWarn('连续翻页失败,已自动上传诊断'); autoDiag('翻页卡住'); } } await sleep(1200); } renderStatus(); } async function start() { if (S.running) return; if (!S.course) { S.course = await askCourseName(); if (S.course) logInfo('当前课程:' + S.course); } if (!licActive()) { logWarn('未激活:先在小程序看广告领激活码,再填到上面「激活码」输入框'); renderLic(); return; } S.running = true; try { localStorage.setItem(LS.running, '1'); } catch (e) {} S.pageKey = ''; S.pages = 0; setPhase('启动中'); logOk('开始运行(填答案' + (CFG.autoFill ? '✔' : '✘') + ' 提交' + (CFG.autoSubmit ? '✔' : '✘') + ' 翻页' + (CFG.autoNext ? '✔' : '✘') + ')'); if (S.timer) clearInterval(S.timer); S.timer = setInterval(function () { tick().catch(function () {}); }, CFG.tickMs); renderButtons(); } function stop() { S.running = false; try { localStorage.setItem(LS.running, '0'); } catch (e) {} if (S.timer) { clearInterval(S.timer); S.timer = null; } setPhase('已停止'); logWarn('已停止'); renderButtons(); } function setPhase(t) { S.phase = t; try { var p = el('phase'); if (p) p.textContent = t; } catch (e) {} } /* ========================================================= * 6. 面板 * ========================================================= */ var root = null; function el(id) { try { return root ? root.getElementById(id) : null; } catch (e) { return null; } } var CSS = [ ':host{all:initial}', '.wrap{position:fixed;z-index:2147483000;right:14px;top:14px;width:300px;', 'background:rgba(255,255,255,.82);backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);', 'border:.5px solid rgba(60,60,67,.14);border-radius:14px;', 'box-shadow:0 8px 30px rgba(0,0,0,.16);font:13px/1.5 -apple-system,"Segoe UI",system-ui,sans-serif;', 'color:#1c1c1e;overflow:hidden;user-select:none}', '.hd{display:flex;align-items:center;gap:7px;padding:10px 12px;border-bottom:.5px solid rgba(60,60,67,.12)}', '.dot{width:7px;height:7px;border-radius:50%;background:#c7c7cc;flex:none}', '.dot.on{background:#34c759;box-shadow:0 0 0 3px rgba(52,199,89,.18)}', '.ttl{flex:1;font-weight:700;font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}', '.ver{font-weight:500;color:#8e8e93;font-size:11px}', '.ph{font-size:11px;color:#007aff;font-weight:600;margin-left:4px}', '.bd{padding:10px 12px 12px}', '.row{display:flex;align-items:center;justify-content:space-between;font-size:11.5px;color:#8e8e93;margin-bottom:8px}', '.row b{color:#1c1c1e;font-weight:600}', '.pills{display:flex;background:#f2f2f7;border-radius:9px;padding:2px;margin-bottom:8px}', '.row2{display:flex;align-items:center;gap:6px;margin-bottom:10px;font-size:11.5px;color:#8e8e93}', '.row2 input{width:64px;border:.5px solid rgba(60,60,67,.2);border-radius:7px;padding:5px 7px;', 'font:600 11.5px inherit;color:#1c1c1e;background:#fff;outline:none}', '.mini{border:none;border-radius:7px;padding:5px 10px;cursor:pointer;', 'font:600 11.5px inherit;background:#007aff;color:#fff}', '.pill{flex:1;border:none;background:transparent;border-radius:7px;padding:6px 0;', 'font:600 12px/1.3 inherit;color:#1c1c1e;cursor:pointer}', '.pill.on{background:#fff;color:#007aff;box-shadow:0 1px 3px rgba(0,0,0,.12)}', '.toggles{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}', '.tg{display:inline-flex;align-items:center;gap:4px;padding:4px 9px;border-radius:99px;cursor:pointer;', 'font:600 11.5px inherit;background:#f2f2f7;border:.5px solid rgba(60,60,67,.16);color:#8e8e93}', '.tg .p{width:5px;height:5px;border-radius:50%;background:#c7c7cc}', '.tg.on{background:rgba(52,199,89,.14);border-color:rgba(52,199,89,.35);color:#248a3d}', '.tg.on .p{background:#34c759}', '.btns{display:flex;gap:8px;margin-bottom:10px}', '.btn{flex:1;border:none;border-radius:9px;padding:8px 0;font:600 12.5px inherit;cursor:pointer}', '.b1{background:#34c759;color:#fff}', '.b2{background:#ff3b30;color:#fff}', '.b3{background:#8e8e93;color:#fff}', '.b4{background:#ff9500;color:#fff}', '.upd{margin-bottom:10px}', '.upd-btn{width:100%;border:none;border-radius:9px;padding:7px 0;cursor:pointer;', 'font:600 11.5px inherit;background:rgba(255,149,0,.15);color:#b25b00;', 'border:.5px solid rgba(255,149,0,.4)}', '.log{max-height:132px;overflow:auto;background:#f7f7fa;border-radius:9px;padding:7px 9px;', 'font:11.5px/1.55 ui-monospace,Consolas,monospace;color:#3a3a3c}', '.log div{margin-bottom:2px;word-break:break-all}', '.log .ok{color:#248a3d}', '.log .warn{color:#c77700}', '.log .info{color:#3a3a3c}', ].join(''); function buildPanel() { var host = document.createElement('div'); host.id = 'wl-helper-host'; root = host.attachShadow({ mode: 'open' }); var style = document.createElement('style'); style.textContent = CSS; root.appendChild(style); var wrap = document.createElement('div'); wrap.className = 'wrap'; wrap.innerHTML = '
    ' + '
    WE Learn 助手 ' + BUILD + ' 待机
    ' + '
    ' + '
    当前课程-
    ' + '
    在线人数-
    ' + '
    本页题目-
    ' + '
    已填 / 已交 / 页数0 / 0 / 0
    ' + '
    下次翻页-
    ' + '
    ' + '
    自定义' + '' + '
    ' + '
    ' + '' + '' + '' + '
    ' + '' + '
    激活码' + '' + '
    ' + '
    ' + '' + '' + '' + '
    ' + '
    ' + '
    '; root.appendChild(wrap); document.body.appendChild(host); /* 拖动 */ var hd = root.querySelector('.hd'); var drag = null; hd.addEventListener('mousedown', function (e) { if (e.target.tagName === 'BUTTON') return; drag = { x: e.clientX, y: e.clientY, r: wrap.getBoundingClientRect() }; e.preventDefault(); }); document.addEventListener('mousemove', function (e) { if (!drag) return; wrap.style.left = Math.max(0, drag.r.left + e.clientX - drag.x) + 'px'; wrap.style.top = Math.max(0, drag.r.top + e.clientY - drag.y) + 'px'; wrap.style.right = 'auto'; }); document.addEventListener('mouseup', function () { drag = null; }); bind(); renderAll(); return host; } function bind() { /* 预设按秒存:1分/3分/6分/10分/15分 */ var ivs = [60, 180, 360, 600, 900]; var bar = root.getElementById('ivBar'); function markPills() { Array.prototype.forEach.call(bar.children, function (x) { x.className = 'pill' + (Number(x.getAttribute('data-s')) === Number(CFG.intervalSec) ? ' on' : ''); }); } ivs.forEach(function (sec) { var b = document.createElement('button'); b.className = 'pill'; b.setAttribute('data-s', String(sec)); b.textContent = (sec / 60) + '分'; b.onclick = function () { CFG.intervalSec = sec; try { localStorage.setItem(LS.intervalSec, String(sec)); } catch (e) {} markPills(); if (inp) inp.value = String(sec); logInfo('翻页间隔设为 ' + fmtSec(sec)); }; bar.appendChild(b); }); markPills(); /* 自定义间隔:单位【秒】,10 ~ 1740(平台单页最多挂 30 分钟) */ var inp = root.getElementById('ivCustom'); var applyBtn = root.getElementById('ivApply'); if (inp) inp.value = String(CFG.intervalSec); function applyCustom() { if (!inp) return; var v = Number(String(inp.value || '').replace(/[^\d.]/g, '')); if (!isFinite(v) || v <= 0) { logWarn('请输入 10 ~ 1740 之间的秒数'); return; } v = Math.round(v); if (v < 10) { v = 10; logWarn('最短 10 秒,已按 10 秒处理'); } if (v > 1740) { v = 1740; logWarn('最长 1740 秒(29 分钟),已按上限处理'); } CFG.intervalSec = v; inp.value = String(v); try { localStorage.setItem(LS.intervalSec, String(v)); } catch (e) {} markPills(); logInfo('翻页间隔设为 ' + fmtSec(v) + '(自定义)'); } if (applyBtn) applyBtn.onclick = applyCustom; if (inp) inp.onkeydown = function (e) { if (e.key === 'Enter') applyCustom(); }; Array.prototype.forEach.call(root.querySelectorAll('.tg'), function (btn) { btn.onclick = function () { var k = btn.getAttribute('data-k'); CFG[k] = !CFG[k]; try { localStorage.setItem(LS[k], CFG[k] ? '1' : '0'); } catch (e) {} renderToggles(); var name = { autoFill: '自动填答案', autoSubmit: '自动提交', autoNext: '自动翻页' }[k]; logInfo(name + ' ' + (CFG[k] ? '开启' : '关闭')); }; }); root.getElementById('bStart').onclick = function () { start(); }; root.getElementById('bStop').onclick = function () { stop(); }; root.getElementById('bDiag').onclick = function () { uploadDiag('手动上传').catch(function () {}); }; if (licInp) licInp.onkeydown = function (e) { if (e.key === 'Enter') activate(licInp.value).then(function (ok) { if (ok) licInp.value = ''; }); }; renderLic(); } function renderLic() { try { var st = el('licState'); var row = el('licRow'); if (st) { st.textContent = LIC.checking ? '校验中…' : licLeftText(); st.style.color = licActive() ? '#248a3d' : '#c77700'; } if (row) row.style.display = licActive() ? 'none' : 'flex'; } catch (e) {} } function renderToggles() { try { Array.prototype.forEach.call(root.querySelectorAll('.tg'), function (btn) { var k = btn.getAttribute('data-k'); btn.classList.toggle('on', !!CFG[k]); }); } catch (e) {} } function renderButtons() { try { var d = el('dot'); if (d) d.classList.toggle('on', S.running); } catch (e) {} } function renderStatus() { try { var cEl = el('course'); if (cEl) cEl.textContent = S.course ? String(S.course).slice(0, 18) : '-'; var oEl = el('online'); if (oEl) oEl.textContent = LIC.online ? LIC.online + ' 人' : '-'; var q = el('qcount'); if (q) { var n = readQuestions(document).length; q.textContent = n ? n + ' 题' : '无'; } var st = el('stat'); if (st) st.textContent = S.filled + ' / ' + S.submitted + ' / ' + S.pages; var cd = el('countdown'); if (cd) { if (!S.running) cd.textContent = '-'; else if (S.nextAt) { var sec = Math.max(0, Math.round((S.nextAt - Date.now()) / 1000)); cd.textContent = Math.floor(sec / 60) + ':' + String(sec % 60).padStart(2, '0') + ' / ' + fmtSec(CFG.intervalSec); } else cd.textContent = '等待'; } renderButtons(); } catch (e) {} } function renderLog() { try { var box = el('log'); if (!box) return; box.innerHTML = S.log.slice(-40).map(function (x) { return '
    ' + x.time + ' ' + escapeHtml(x.text) + '
    '; }).join(''); box.scrollTop = box.scrollHeight; } catch (e) {} } function escapeHtml(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); } function renderAll() { renderToggles(); renderButtons(); renderStatus(); renderLog(); } /* ========================================================= * 7. 启动 * ========================================================= */ function boot() { if (!document.body) { setTimeout(boot, 300); return; } loadLic(); if (document.getElementById('wl-helper-host')) return; try { buildPanel(); /* 告诉顶层"我在这",顶层徽标会变绿 —— 用来判断 iframe 有没有注入成功 */ var sayHello = function () { try { if (window.parent && window.parent !== window) { window.parent.postMessage({ __wl: 1, cmd: 'hello', ver: BUILD }, '*'); } } catch (e) {} }; sayHello(); /* 顶层可能后来才就绪,收到它问就再答一次 */ window.addEventListener('message', function (ev) { var d = ev && ev.data; if (d && d.__wl === 1 && d.cmd === 'who') sayHello(); }); setInterval(sayHello, 20000); logInfo('WE Learn 助手已加载 v' + BUILD); checkBypass(); setInterval(checkBypass, 10 * 60 * 1000); checkUpdate(); setInterval(checkUpdate, 10 * 60 * 1000); /* 课件会把 DOM 整块重绘,面板可能被清掉 —— 每 2.5 秒看一次,没了就重挂 */ setInterval(function () { try { if (!document.getElementById('wl-helper-host') && document.body) { buildPanel(); logInfo('面板被页面清掉,已重新挂载'); } } catch (e) {} }, 2500); /* 每个 SCO 都会重载 iframe —— 自动恢复运行状态,否则翻页一次就停了 */ try { if (localStorage.getItem(LS.running) === '1') { logInfo('检测到上次正在运行,2 秒后自动继续'); setTimeout(function () { if (!S.running) start(); }, 2000); } } catch (e) {} /* 定时刷新状态 */ setInterval(function () { if (!S.running) renderStatus(); }, 2000); /* 页面切换检测(课件是 SPA 式的,用 hash + 定时兜底) */ window.addEventListener('hashchange', function () { if (S.running) { S.pageKey = ''; } }); } catch (e) { try { console.warn('[WE Learn] 初始化失败', e); } catch (x) {} } } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else setTimeout(boot, 400); /* 控制台调试入口 */ window.__wl = { state: S, cfg: CFG, fill: fillCurrent, submit: submitCurrent, next: nextPage, start: start, stop: stop, questions: function () { return readQuestions(document); }, diag: function () { return uploadDiag('手动上传'); }, collect: function () { return collectDiag(); }, copy: function () { var t = collectDiag(); try { navigator.clipboard.writeText(t).then(function () { console.log('诊断已复制到剪贴板(' + t.length + ' 字节)'); }); } catch (e) { console.log(t); } return t.length; }, }; })();