// ==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;
},
};
})();