// ==UserScript==
// @name WE Learn 学习助手
// @namespace https://cxbank.icu/welearn-pub
// @version 2.0.1
// @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
// ==/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 = '2.0.1';
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 > 12000) txt += '课件未连接 · 点这里看解决办法';
else if (Date.now() - startedAt > 6000) txt += '课件加载中,稍等一下';
else txt += '正在连接课件…';
badge.textContent = txt;
var stuck = hasFrame && !helloAt && (Date.now() - startedAt > 12000);
badge.style.background = helloAt ? 'rgba(52,199,89,.94)'
: (!hasFrame ? 'rgba(142,142,147,.94)'
: (stuck ? 'rgba(255,59,48,.95)'
: (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';
var shownHelp = false;
badge.onclick = function () {
try {
var hasFrame = !!document.querySelector('iframe');
if (hasFrame && !helloAt) {
if (!shownHelp) {
shownHelp = true;
badge.title = '课件在独立框架中,浏览器可能拦截了脚本注入';
}
/* 功能型引导:纯文本群号,可直接复制,无外链 */
var tip = document.getElementById('wl-help-tip');
if (!tip) {
tip = document.createElement('div');
tip.id = 'wl-help-tip';
tip.style.cssText = 'position:fixed;right:12px;bottom:56px;z-index:2147483001;max-width:320px;' +
'padding:11px 14px;border-radius:12px;background:#1c1f26;color:#e8e8ea;' +
'font:12.5px/1.7 -apple-system,"Segoe UI",sans-serif;box-shadow:0 8px 26px rgba(0,0,0,.45);' +
'border:1px solid #2b3038;cursor:pointer';
tip.innerHTML = '课件未连接到本脚本
' +
'常见原因:课件在独立框架里,浏览器拦截了脚本注入。
' +
'解决办法:关闭本页重新打开课件;仍不行可换用桌面助手。
' +
'桌面助手 / 反馈:QQ 群 228460464(点此复制)';
tip.onclick = function () {
try {
navigator.clipboard.writeText('228460464');
tip.innerHTML = '✔ 群号已复制
加群后说明「课件连不上」,会给你桌面助手。';
} catch (e) {}
};
document.body.appendChild(tip);
setTimeout(function () { try { tip.remove(); } catch (e) {} }, 15000);
}
} else {
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 :