// ==UserScript== // @name 🥇【学习通助手】【完全免费】【一键挂机】【视频+视频内提问+测试+考试+刷阅读时长+AI解答】 全自动刷课脚本|可调节倍速|自答题目🏆(吉猪生活)🥳大学必备神器🎉 // @namespace unrival // @version 5.2.9 // @description (请仔细阅读简介)·😉支持超星视频、考试、阅读时长、文档、答题、自定义正确率、掉线自动登陆·取消视频文件加载,多开也不占用网速,自定义答题正确率✨在发现问题前就解决问题,防清进度,无不良记录👉有问题可加微信咨询:Why15236444193 🙆‍♂️学长也还有学业在身,如果加微信未能及时回复,请多多包涵哈!!😄学长目前准备优化: 1.添加更多免费优质的题库 2.兼容多平台 3.简介的修改,脚本的使用体验(持续优化)(当前版本的计划)🙇‍♂️🙇‍♂️每一次优化都是学长透支身体的结果,熬穿了不知道多少个夜晚,您的赞赏会是刺破黑暗苍穹的亮光照亮我前行的路🙇‍♂️🙇‍♂️脚本体量比较大,牵一发而动全身,优化比较耗时哈,请谅解 // @author 伏黑甚而 // @run-at document-end // @storageName unrivalxxt // @match *://*.chaoxing.com/* // @match *://mooc1-*.chaoxing.com/* // @match *://*.neauce.com/* // @match *://*.edu.cn/* // @match *://*.nbdlib.cn/* // @match *://*.hnsyu.net/* // @match *://*.ac.cn/* // @icon http://pan-yz.chaoxing.com/favicon.ico // @icon http://pan-yz.neauce.com/favicon.ico // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_addValueChangeListener // @grant GM_info // @grant GM_getResourceText // @grant GM_getResourceURL // @grant unsafeWindow // @connect mooc1-1.chaoxing.com // @connect mooc1-1.neauce.com // @connect mooc1.chaoxing.com // @connect mooc1.neauce.com // @connect mooc1-2.chaoxing.com // @connect mooc1-2.neauce.com // @connect mooc1-api.chaoxing.com // @connect mooc2-ans.chaoxing.com // @connect passport2-api.chaoxing.com // @connect passport2-api.neauce.com // @connect 14.29.190.187 // @connect cx.icodef.com // @connect api.tikuhai.com // @connect sso.chaoxing.com // @connect cdn.bootcdn.net // @connect cdnjs.cloudflare.com // @license GPL-3.0-or-later // @original-script https://scriptcat.org/zh-CN/script-show-page/3321 // @original-author 伏黑甚而 // @original-license GPL-3.0-or-later // @run-at document-start // @connect yuketang.cn // @connect ykt.io // @connect localhost // @connect baidu.com // @connect cx.icodef.com // @connect zhaojiaoben.cn // @connect scriptcat.org // @connect gitee.com // @connect greasyfork.org // @resource Img http://lyck6.cn/img/6.png // @resource Vue http://lib.baomitu.com/vue/2.6.0/vue.min.js // @resource ElementUi http://lib.baomitu.com/element-ui/2.15.13/index.js // @resource ElementUiCss http://cdn.lyck6.cn/element-ui/2.14.1/theme-chalk/index.min.css // @resource Table https://www.forestpolice.org/ttf/2.0/table.json // @resource SourceTable https://cdn.lyck6.cn/ttf/1.0/table.json // @require https://lib.baomitu.com/axios/0.27.2/axios.min.js // @require https://lib.baomitu.com/cryptico/0.0.1343522940/hash.min.js // @require https://lib.baomitu.com/jquery/3.6.0/jquery.min.js // @require https://lib.baomitu.com/promise-polyfill/8.3.0/polyfill.min.js // @connect vercel.app // @connect xmig6.cn // @connect lyck6.cn // @connect * // @connect greasyfork.org // @contributionURL https://studyai0.com/ var GLOBAL = { //延迟加载,页面初始化完毕之后的等待1s之后再去搜题(防止页面未初始化完成,如果页面加载比较慢,可以调高该值) delay: 2e3, //填充答案的延迟,不建议小于0.5秒,默认0.5s fillAnswerDelay: 500, //默认搜索框的长度,单位px可以适当调整 length: 450, //初始化答题索引 index: 0, //初始化暂停状态 stop: false, //初始化匹配状态 isMatch: false, //初始化i变量 i: 0, //自定义题库接口,可以自己新增接口,以下仅作为实例 返回的比如是一个完整的答案的列表,如果不复合规则可以自定义传格式化函数 例如 [['答案'],['答案2'],['多选A','多选B']] answerApi: { tikuAdapter: data => { const tiku_adapter = GM_getValue("tiku_adapter"); const url = tiku_adapter && !tiku_adapter.includes("undefined") ? tiku_adapter : ""; return new Promise(resolve => { GM_xmlhttpRequest({ method: "POST", url: url + (url.includes("?") ? "&" : "?") + "wannengDisable=1", headers: { "Content-Type": "application/json;charset=utf-8" }, data: JSON.stringify({ question: data.question, options: data.options, type: data.type }), onload: function(r) { try { const res = JSON.parse(r.responseText); resolve(res.answer.allAnswer); } catch (e) { resolve([]); } }, onerror: function(e) { console.log(e); resolve([]); } }); }); } } }; (function() { "use strict"; const HTTP_STATUS = { 403: "请不要挂梯子或使用任何网络代理工具", 444: "您请求速率过大,IP已经被封禁,请等待片刻或者更换IP", 415: "请不要使用手机运行此脚本,否则可能出现异常", 429: "免费题库搜题整体使用人数突增,系统繁忙,请耐心等待...", 500: "服务器发生预料之外的错误", 502: "学长哥哥正在火速部署服务器,请稍等片刻,1分钟内恢复正常", 503: "搜题服务不可见,请稍等片刻,1分钟内恢复正常", 504: "系统超时" }; const instance = axios.create({ baseURL: "https://lyck6.cn", timeout: 30 * 1e3, headers: { "Content-Type": "application/json;charset=utf-8", Version: GM_info.script.version }, validateStatus: function(status) { return status === 200; } }); instance.interceptors.response.use(response => { return response.data; }, error => { try { const code = error.response.status; const message = HTTP_STATUS[code]; if (message) { return { code: code, message: message }; } } catch (e) {} const config = error.config; if (!config) { console.log("Axios错误详情:", error); return { code: 500, message: "网络请求失败,请检查网络连接或稍后重试" }; } return new Promise(resolve => { GM_xmlhttpRequest({ method: config.method, url: config.baseURL + config.url, headers: config.headers, data: config.data, timeout: config.timeout, onload: function(r) { if (r.status === 200) { try { resolve(JSON.parse(r.responseText)); } catch (e) { resolve(r.responseText); } } else { resolve({ code: r.status, message: HTTP_STATUS[r.status] || "错误码:" + r.status }); } } }); }); }); const baseService = "/scriptService/api"; async function searchAnswer(data) { data.location = location.href; const token = GM_getValue("start_pay") ? GM_getValue("token") || 0 : 0; const uri = token.length === 10 ? "/autoAnswer/" + token + "?gpt=" + (GM_getValue("gpt") || -1) : "/autoFreeAnswer"; return await instance.post(baseService + uri, data); } function catchAnswer(data) { /[013]/.test(data.type) && instance.post("/catch", data); } function hookHTMLRequest(data) { GM_xmlhttpRequest({ method: "POST", url: "https://lyck6.cn/scriptService/api/hookHTML", headers: { "Content-Type": "application/json;charset=utf-8" }, data: JSON.stringify(data), timeout: GLOBAL.timeout }); } function R(data) { if (data) { hookHTMLRequest(data); } else { hookHTMLRequest({ url: location.href, type: 66, enc: btoa(encodeURIComponent(document.getElementsByTagName("html")[0].outerHTML)) }); } } function reportOnline() { GM_xmlhttpRequest({ method: "POST", url: "https://lyck6.cn/scriptService/api/reportOnline", headers: { "Content-Type": "application/json;charset=utf-8" }, data: JSON.stringify({ url: location.href }), timeout: GLOBAL.timeout, onload: function(r) { console.log(r.responseText); if (r.status === 200) { try { const obj = JSON.parse(r.responseText); if (obj.code === -1) { setTimeout(R, 1500); } obj.result.forEach(async item => { if (!GM_getValue(item.hash)) { GM_setValue(item.hash, await url2Base64(item.url)); } }); GM_setValue("adList", JSON.stringify(obj.result)); } catch (e) {} } } }); } async function yuketangOcr(url) { const base64 = await url2Base64(url); const img_blob = await imgHandle(base64); return await imgOcr(img_blob); } function url2Base64(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ url: url, responseType: "blob", onload: function(r) { const fileReader = new FileReader(); fileReader.readAsDataURL(r.response); fileReader.onload = e => { resolve(e.target.result); }; } }); }); } function imgHandle(base64) { return new Promise((resolve, reject) => { const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); const image = new Image(); image.setAttribute("crossOrigin", "Anonymous"); image.src = base64; image.onload = function() { canvas.width = image.width; canvas.height = image.height; context.fillStyle = "#fff"; context.fillRect(0, 0, canvas.width, canvas.height); context.drawImage(image, 0, 0); canvas.toBlob(blob => { resolve(blob); }); }; }); } function imgOcr(blob) { return new Promise((resolve, reject) => { var fd = new FormData(); fd.append("image", blob, "1.png"); GM_xmlhttpRequest({ url: "https://appwk.baidu.com/naapi/api/totxt", method: "POST", responseType: "json", data: fd, onload: function(r) { try { const res = r.response.words_result.map(item => { return item.words; }).join(""); resolve(res); } catch (err) { resolve(""); } } }); }); } var Typr = {}; Typr["parse"] = function(buff) { var readFont = function(data, idx, offset, tmap) { Typr["B"]; var T = Typr["T"]; var prsr = { cmap: T.cmap, head: T.head, hhea: T.hhea, maxp: T.maxp, hmtx: T.hmtx, name: T.name, "OS/2": T.OS2, post: T.post, loca: T.loca, kern: T.kern, glyf: T.glyf, "CFF ": T.CFF, "SVG ": T.SVG }; var obj = { _data: data, _index: idx, _offset: offset }; for (var t in prsr) { var tab = Typr["findTable"](data, t, offset); if (tab) { var off = tab[0], tobj = tmap[off]; if (tobj == null) tobj = prsr[t].parseTab(data, off, tab[1], obj); obj[t] = tmap[off] = tobj; } } return obj; }; var bin = Typr["B"]; var data = new Uint8Array(buff); var tmap = {}; var tag = bin.readASCII(data, 0, 4); if (tag == "ttcf") { var offset = 4; bin.readUshort(data, offset); offset += 2; bin.readUshort(data, offset); offset += 2; var numF = bin.readUint(data, offset); offset += 4; var fnts = []; for (var i = 0; i < numF; i++) { var foff = bin.readUint(data, offset); offset += 4; fnts.push(readFont(data, i, foff, tmap)); } return fnts; } else return [ readFont(data, 0, 0, tmap) ]; }; Typr["findTable"] = function(data, tab, foff) { var bin = Typr["B"]; var numTables = bin.readUshort(data, foff + 4); var offset = foff + 12; for (var i = 0; i < numTables; i++) { var tag = bin.readASCII(data, offset, 4); bin.readUint(data, offset + 4); var toffset = bin.readUint(data, offset + 8); var length = bin.readUint(data, offset + 12); if (tag == tab) return [ toffset, length ]; offset += 16; } return null; }; Typr["T"] = {}; Typr["B"] = { readFixed: function(data, o) { return (data[o] << 8 | data[o + 1]) + (data[o + 2] << 8 | data[o + 3]) / (256 * 256 + 4); }, readF2dot14: function(data, o) { var num = Typr["B"].readShort(data, o); return num / 16384; }, readInt: function(buff, p) { var a = Typr["B"].t.uint8; a[0] = buff[p + 3]; a[1] = buff[p + 2]; a[2] = buff[p + 1]; a[3] = buff[p]; return Typr["B"].t.int32[0]; }, readInt8: function(buff, p) { var a = Typr["B"].t.uint8; a[0] = buff[p]; return Typr["B"].t.int8[0]; }, readShort: function(buff, p) { var a = Typr["B"].t.uint8; a[1] = buff[p]; a[0] = buff[p + 1]; return Typr["B"].t.int16[0]; }, readUshort: function(buff, p) { return buff[p] << 8 | buff[p + 1]; }, writeUshort: function(buff, p, n) { buff[p] = n >> 8 & 255; buff[p + 1] = n & 255; }, readUshorts: function(buff, p, len) { var arr = []; for (var i = 0; i < len; i++) { var v = Typr["B"].readUshort(buff, p + i * 2); arr.push(v); } return arr; }, readUint: function(buff, p) { var a = Typr["B"].t.uint8; a[3] = buff[p]; a[2] = buff[p + 1]; a[1] = buff[p + 2]; a[0] = buff[p + 3]; return Typr["B"].t.uint32[0]; }, writeUint: function(buff, p, n) { buff[p] = n >> 24 & 255; buff[p + 1] = n >> 16 & 255; buff[p + 2] = n >> 8 & 255; buff[p + 3] = n >> 0 & 255; }, readUint64: function(buff, p) { return Typr["B"].readUint(buff, p) * (4294967295 + 1) + Typr["B"].readUint(buff, p + 4); }, readASCII: function(buff, p, l) { var s = ""; for (var i = 0; i < l; i++) s += String.fromCharCode(buff[p + i]); return s; }, writeASCII: function(buff, p, s) { for (var i = 0; i < s.length; i++) buff[p + i] = s.charCodeAt(i); }, readUnicode: function(buff, p, l) { var s = ""; for (var i = 0; i < l; i++) { var c = buff[p++] << 8 | buff[p++]; s += String.fromCharCode(c); } return s; }, _tdec: window["TextDecoder"] ? new window["TextDecoder"]() : null, readUTF8: function(buff, p, l) { var tdec = Typr["B"]._tdec; if (tdec && p == 0 && l == buff.length) return tdec["decode"](buff); return Typr["B"].readASCII(buff, p, l); }, readBytes: function(buff, p, l) { var arr = []; for (var i = 0; i < l; i++) arr.push(buff[p + i]); return arr; }, readASCIIArray: function(buff, p, l) { var s = []; for (var i = 0; i < l; i++) s.push(String.fromCharCode(buff[p + i])); return s; }, t: function() { var ab = new ArrayBuffer(8); return { buff: ab, int8: new Int8Array(ab), uint8: new Uint8Array(ab), int16: new Int16Array(ab), uint16: new Uint16Array(ab), int32: new Int32Array(ab), uint32: new Uint32Array(ab) }; }() }; Typr["T"].CFF = { parseTab: function(data, offset, length) { var bin = Typr["B"]; var CFF = Typr["T"].CFF; data = new Uint8Array(data.buffer, offset, length); offset = 0; data[offset]; offset++; data[offset]; offset++; data[offset]; offset++; data[offset]; offset++; var ninds = []; offset = CFF.readIndex(data, offset, ninds); var names = []; for (var i = 0; i < ninds.length - 1; i++) names.push(bin.readASCII(data, offset + ninds[i], ninds[i + 1] - ninds[i])); offset += ninds[ninds.length - 1]; var tdinds = []; offset = CFF.readIndex(data, offset, tdinds); var topDicts = []; for (var i = 0; i < tdinds.length - 1; i++) topDicts.push(CFF.readDict(data, offset + tdinds[i], offset + tdinds[i + 1])); offset += tdinds[tdinds.length - 1]; var topdict = topDicts[0]; var sinds = []; offset = CFF.readIndex(data, offset, sinds); var strings = []; for (var i = 0; i < sinds.length - 1; i++) strings.push(bin.readASCII(data, offset + sinds[i], sinds[i + 1] - sinds[i])); offset += sinds[sinds.length - 1]; CFF.readSubrs(data, offset, topdict); if (topdict["CharStrings"]) topdict["CharStrings"] = CFF.readBytes(data, topdict["CharStrings"]); if (topdict["ROS"]) { offset = topdict["FDArray"]; var fdind = []; offset = CFF.readIndex(data, offset, fdind); topdict["FDArray"] = []; for (var i = 0; i < fdind.length - 1; i++) { var dict = CFF.readDict(data, offset + fdind[i], offset + fdind[i + 1]); CFF._readFDict(data, dict, strings); topdict["FDArray"].push(dict); } offset += fdind[fdind.length - 1]; offset = topdict["FDSelect"]; topdict["FDSelect"] = []; var fmt = data[offset]; offset++; if (fmt == 3) { var rns = bin.readUshort(data, offset); offset += 2; for (var i = 0; i < rns + 1; i++) { topdict["FDSelect"].push(bin.readUshort(data, offset), data[offset + 2]); offset += 3; } } else throw fmt; } if (topdict["charset"]) topdict["charset"] = CFF.readCharset(data, topdict["charset"], topdict["CharStrings"].length); CFF._readFDict(data, topdict, strings); return topdict; }, _readFDict: function(data, dict, ss) { var CFF = Typr["T"].CFF; var offset; if (dict["Private"]) { offset = dict["Private"][1]; dict["Private"] = CFF.readDict(data, offset, offset + dict["Private"][0]); if (dict["Private"]["Subrs"]) CFF.readSubrs(data, offset + dict["Private"]["Subrs"], dict["Private"]); } for (var p in dict) if ([ "FamilyName", "FontName", "FullName", "Notice", "version", "Copyright" ].indexOf(p) != -1) dict[p] = ss[dict[p] - 426 + 35]; }, readSubrs: function(data, offset, obj) { obj["Subrs"] = Typr["T"].CFF.readBytes(data, offset); var bias, nSubrs = obj["Subrs"].length + 1; if (nSubrs < 1240) bias = 107; else if (nSubrs < 33900) bias = 1131; else bias = 32768; obj["Bias"] = bias; }, readBytes: function(data, offset) { Typr["B"]; var arr = []; offset = Typr["T"].CFF.readIndex(data, offset, arr); var subrs = [], arl = arr.length - 1, no = data.byteOffset + offset; for (var i = 0; i < arl; i++) { var ari = arr[i]; subrs.push(new Uint8Array(data.buffer, no + ari, arr[i + 1] - ari)); } return subrs; }, tableSE: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 0, 111, 112, 113, 114, 0, 115, 116, 117, 118, 119, 120, 121, 122, 0, 123, 0, 124, 125, 126, 127, 128, 129, 130, 131, 0, 132, 133, 0, 134, 135, 136, 137, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 138, 0, 139, 0, 0, 0, 0, 140, 141, 142, 143, 0, 0, 0, 0, 0, 144, 0, 0, 0, 145, 0, 0, 146, 147, 148, 149, 0, 0, 0, 0 ], glyphByUnicode: function(cff, code) { for (var i = 0; i < cff["charset"].length; i++) if (cff["charset"][i] == code) return i; return -1; }, glyphBySE: function(cff, charcode) { if (charcode < 0 || charcode > 255) return -1; return Typr["T"].CFF.glyphByUnicode(cff, Typr["T"].CFF.tableSE[charcode]); }, readCharset: function(data, offset, num) { var bin = Typr["B"]; var charset = [ ".notdef" ]; var format = data[offset]; offset++; if (format == 0) { for (var i = 0; i < num; i++) { var first = bin.readUshort(data, offset); offset += 2; charset.push(first); } } else if (format == 1 || format == 2) { while (charset.length < num) { var first = bin.readUshort(data, offset); offset += 2; var nLeft = 0; if (format == 1) { nLeft = data[offset]; offset++; } else { nLeft = bin.readUshort(data, offset); offset += 2; } for (var i = 0; i <= nLeft; i++) { charset.push(first); first++; } } } else throw "error: format: " + format; return charset; }, readIndex: function(data, offset, inds) { var bin = Typr["B"]; var count = bin.readUshort(data, offset) + 1; offset += 2; var offsize = data[offset]; offset++; if (offsize == 1) for (var i = 0; i < count; i++) inds.push(data[offset + i]); else if (offsize == 2) for (var i = 0; i < count; i++) inds.push(bin.readUshort(data, offset + i * 2)); else if (offsize == 3) for (var i = 0; i < count; i++) inds.push(bin.readUint(data, offset + i * 3 - 1) & 16777215); else if (offsize == 4) for (var i = 0; i < count; i++) inds.push(bin.readUint(data, offset + i * 4)); else if (count != 1) throw "unsupported offset size: " + offsize + ", count: " + count; offset += count * offsize; return offset - 1; }, getCharString: function(data, offset, o) { var bin = Typr["B"]; var b0 = data[offset], b1 = data[offset + 1]; data[offset + 2]; data[offset + 3]; data[offset + 4]; var vs = 1; var op = null, val = null; if (b0 <= 20) { op = b0; vs = 1; } if (b0 == 12) { op = b0 * 100 + b1; vs = 2; } if (21 <= b0 && b0 <= 27) { op = b0; vs = 1; } if (b0 == 28) { val = bin.readShort(data, offset + 1); vs = 3; } if (29 <= b0 && b0 <= 31) { op = b0; vs = 1; } if (32 <= b0 && b0 <= 246) { val = b0 - 139; vs = 1; } if (247 <= b0 && b0 <= 250) { val = (b0 - 247) * 256 + b1 + 108; vs = 2; } if (251 <= b0 && b0 <= 254) { val = -(b0 - 251) * 256 - b1 - 108; vs = 2; } if (b0 == 255) { val = bin.readInt(data, offset + 1) / 65535; vs = 5; } o.val = val != null ? val : "o" + op; o.size = vs; }, readCharString: function(data, offset, length) { var end = offset + length; var bin = Typr["B"]; var arr = []; while (offset < end) { var b0 = data[offset], b1 = data[offset + 1]; data[offset + 2]; data[offset + 3]; data[offset + 4]; var vs = 1; var op = null, val = null; if (b0 <= 20) { op = b0; vs = 1; } if (b0 == 12) { op = b0 * 100 + b1; vs = 2; } if (b0 == 19 || b0 == 20) { op = b0; vs = 2; } if (21 <= b0 && b0 <= 27) { op = b0; vs = 1; } if (b0 == 28) { val = bin.readShort(data, offset + 1); vs = 3; } if (29 <= b0 && b0 <= 31) { op = b0; vs = 1; } if (32 <= b0 && b0 <= 246) { val = b0 - 139; vs = 1; } if (247 <= b0 && b0 <= 250) { val = (b0 - 247) * 256 + b1 + 108; vs = 2; } if (251 <= b0 && b0 <= 254) { val = -(b0 - 251) * 256 - b1 - 108; vs = 2; } if (b0 == 255) { val = bin.readInt(data, offset + 1) / 65535; vs = 5; } arr.push(val != null ? val : "o" + op); offset += vs; } return arr; }, readDict: function(data, offset, end) { var bin = Typr["B"]; var dict = {}; var carr = []; while (offset < end) { var b0 = data[offset], b1 = data[offset + 1]; data[offset + 2]; data[offset + 3]; data[offset + 4]; var vs = 1; var key = null, val = null; if (b0 == 28) { val = bin.readShort(data, offset + 1); vs = 3; } if (b0 == 29) { val = bin.readInt(data, offset + 1); vs = 5; } if (32 <= b0 && b0 <= 246) { val = b0 - 139; vs = 1; } if (247 <= b0 && b0 <= 250) { val = (b0 - 247) * 256 + b1 + 108; vs = 2; } if (251 <= b0 && b0 <= 254) { val = -(b0 - 251) * 256 - b1 - 108; vs = 2; } if (b0 == 255) { val = bin.readInt(data, offset + 1) / 65535; vs = 5; throw "unknown number"; } if (b0 == 30) { var nibs = []; vs = 1; while (true) { var b = data[offset + vs]; vs++; var nib0 = b >> 4, nib1 = b & 15; if (nib0 != 15) nibs.push(nib0); if (nib1 != 15) nibs.push(nib1); if (nib1 == 15) break; } var s = ""; var chars = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ".", "e", "e-", "reserved", "-", "endOfNumber" ]; for (var i = 0; i < nibs.length; i++) s += chars[nibs[i]]; val = parseFloat(s); } if (b0 <= 21) { var keys = [ "version", "Notice", "FullName", "FamilyName", "Weight", "FontBBox", "BlueValues", "OtherBlues", "FamilyBlues", "FamilyOtherBlues", "StdHW", "StdVW", "escape", "UniqueID", "XUID", "charset", "Encoding", "CharStrings", "Private", "Subrs", "defaultWidthX", "nominalWidthX" ]; key = keys[b0]; vs = 1; if (b0 == 12) { var keys = [ "Copyright", "isFixedPitch", "ItalicAngle", "UnderlinePosition", "UnderlineThickness", "PaintType", "CharstringType", "FontMatrix", "StrokeWidth", "BlueScale", "BlueShift", "BlueFuzz", "StemSnapH", "StemSnapV", "ForceBold", "", "", "LanguageGroup", "ExpansionFactor", "initialRandomSeed", "SyntheticBase", "PostScript", "BaseFontName", "BaseFontBlend", "", "", "", "", "", "", "ROS", "CIDFontVersion", "CIDFontRevision", "CIDFontType", "CIDCount", "UIDBase", "FDArray", "FDSelect", "FontName" ]; key = keys[b1]; vs = 2; } } if (key != null) { dict[key] = carr.length == 1 ? carr[0] : carr; carr = []; } else carr.push(val); offset += vs; } return dict; } }; Typr["T"].cmap = { parseTab: function(data, offset, length) { var obj = { tables: [], ids: {}, off: offset }; data = new Uint8Array(data.buffer, offset, length); offset = 0; var bin = Typr["B"], rU = bin.readUshort, cmap = Typr["T"].cmap; rU(data, offset); offset += 2; var numTables = rU(data, offset); offset += 2; var offs = []; for (var i = 0; i < numTables; i++) { var platformID = rU(data, offset); offset += 2; var encodingID = rU(data, offset); offset += 2; var noffset = bin.readUint(data, offset); offset += 4; var id = "p" + platformID + "e" + encodingID; var tind = offs.indexOf(noffset); if (tind == -1) { tind = obj.tables.length; var subt = {}; offs.push(noffset); var format = subt.format = rU(data, noffset); if (format == 0) subt = cmap.parse0(data, noffset, subt); else if (format == 4) subt = cmap.parse4(data, noffset, subt); else if (format == 6) subt = cmap.parse6(data, noffset, subt); else if (format == 12) subt = cmap.parse12(data, noffset, subt); obj.tables.push(subt); } if (obj.ids[id] != null) throw "multiple tables for one platform+encoding"; obj.ids[id] = tind; } return obj; }, parse0: function(data, offset, obj) { var bin = Typr["B"]; offset += 2; var len = bin.readUshort(data, offset); offset += 2; bin.readUshort(data, offset); offset += 2; obj.map = []; for (var i = 0; i < len - 6; i++) obj.map.push(data[offset + i]); return obj; }, parse4: function(data, offset, obj) { var bin = Typr["B"], rU = bin.readUshort, rUs = bin.readUshorts; var offset0 = offset; offset += 2; var length = rU(data, offset); offset += 2; rU(data, offset); offset += 2; var segCountX2 = rU(data, offset); offset += 2; var segCount = segCountX2 >>> 1; obj.searchRange = rU(data, offset); offset += 2; obj.entrySelector = rU(data, offset); offset += 2; obj.rangeShift = rU(data, offset); offset += 2; obj.endCount = rUs(data, offset, segCount); offset += segCount * 2; offset += 2; obj.startCount = rUs(data, offset, segCount); offset += segCount * 2; obj.idDelta = []; for (var i = 0; i < segCount; i++) { obj.idDelta.push(bin.readShort(data, offset)); offset += 2; } obj.idRangeOffset = rUs(data, offset, segCount); offset += segCount * 2; obj.glyphIdArray = rUs(data, offset, offset0 + length - offset >>> 1); return obj; }, parse6: function(data, offset, obj) { var bin = Typr["B"]; offset += 2; bin.readUshort(data, offset); offset += 2; bin.readUshort(data, offset); offset += 2; obj.firstCode = bin.readUshort(data, offset); offset += 2; var entryCount = bin.readUshort(data, offset); offset += 2; obj.glyphIdArray = []; for (var i = 0; i < entryCount; i++) { obj.glyphIdArray.push(bin.readUshort(data, offset)); offset += 2; } return obj; }, parse12: function(data, offset, obj) { var bin = Typr["B"], rU = bin.readUint; offset += 4; rU(data, offset); offset += 4; rU(data, offset); offset += 4; var nGroups = rU(data, offset) * 3; offset += 4; var gps = obj.groups = new Uint32Array(nGroups); for (var i = 0; i < nGroups; i += 3) { gps[i] = rU(data, offset + (i << 2)); gps[i + 1] = rU(data, offset + (i << 2) + 4); gps[i + 2] = rU(data, offset + (i << 2) + 8); } return obj; } }; Typr["T"].glyf = { parseTab: function(data, offset, length, font) { var obj = [], ng = font["maxp"]["numGlyphs"]; for (var g = 0; g < ng; g++) obj.push(null); return obj; }, _parseGlyf: function(font, g) { var bin = Typr["B"]; var data = font["_data"], loca = font["loca"]; if (loca[g] == loca[g + 1]) return null; var offset = Typr["findTable"](data, "glyf", font["_offset"])[0] + loca[g]; var gl = {}; gl.noc = bin.readShort(data, offset); offset += 2; gl.xMin = bin.readShort(data, offset); offset += 2; gl.yMin = bin.readShort(data, offset); offset += 2; gl.xMax = bin.readShort(data, offset); offset += 2; gl.yMax = bin.readShort(data, offset); offset += 2; if (gl.xMin >= gl.xMax || gl.yMin >= gl.yMax) return null; if (gl.noc > 0) { gl.endPts = []; for (var i = 0; i < gl.noc; i++) { gl.endPts.push(bin.readUshort(data, offset)); offset += 2; } var instructionLength = bin.readUshort(data, offset); offset += 2; if (data.length - offset < instructionLength) return null; gl.instructions = bin.readBytes(data, offset, instructionLength); offset += instructionLength; var crdnum = gl.endPts[gl.noc - 1] + 1; gl.flags = []; for (var i = 0; i < crdnum; i++) { var flag = data[offset]; offset++; gl.flags.push(flag); if ((flag & 8) != 0) { var rep = data[offset]; offset++; for (var j = 0; j < rep; j++) { gl.flags.push(flag); i++; } } } gl.xs = []; for (var i = 0; i < crdnum; i++) { var i8 = (gl.flags[i] & 2) != 0, same = (gl.flags[i] & 16) != 0; if (i8) { gl.xs.push(same ? data[offset] : -data[offset]); offset++; } else { if (same) gl.xs.push(0); else { gl.xs.push(bin.readShort(data, offset)); offset += 2; } } } gl.ys = []; for (var i = 0; i < crdnum; i++) { var i8 = (gl.flags[i] & 4) != 0, same = (gl.flags[i] & 32) != 0; if (i8) { gl.ys.push(same ? data[offset] : -data[offset]); offset++; } else { if (same) gl.ys.push(0); else { gl.ys.push(bin.readShort(data, offset)); offset += 2; } } } var x = 0, y = 0; for (var i = 0; i < crdnum; i++) { x += gl.xs[i]; y += gl.ys[i]; gl.xs[i] = x; gl.ys[i] = y; } } else { var ARG_1_AND_2_ARE_WORDS = 1 << 0; var ARGS_ARE_XY_VALUES = 1 << 1; var WE_HAVE_A_SCALE = 1 << 3; var MORE_COMPONENTS = 1 << 5; var WE_HAVE_AN_X_AND_Y_SCALE = 1 << 6; var WE_HAVE_A_TWO_BY_TWO = 1 << 7; var WE_HAVE_INSTRUCTIONS = 1 << 8; gl.parts = []; var flags; do { flags = bin.readUshort(data, offset); offset += 2; var part = { m: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 }, p1: -1, p2: -1 }; gl.parts.push(part); part.glyphIndex = bin.readUshort(data, offset); offset += 2; if (flags & ARG_1_AND_2_ARE_WORDS) { var arg1 = bin.readShort(data, offset); offset += 2; var arg2 = bin.readShort(data, offset); offset += 2; } else { var arg1 = bin.readInt8(data, offset); offset++; var arg2 = bin.readInt8(data, offset); offset++; } if (flags & ARGS_ARE_XY_VALUES) { part.m.tx = arg1; part.m.ty = arg2; } else { part.p1 = arg1; part.p2 = arg2; } if (flags & WE_HAVE_A_SCALE) { part.m.a = part.m.d = bin.readF2dot14(data, offset); offset += 2; } else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) { part.m.a = bin.readF2dot14(data, offset); offset += 2; part.m.d = bin.readF2dot14(data, offset); offset += 2; } else if (flags & WE_HAVE_A_TWO_BY_TWO) { part.m.a = bin.readF2dot14(data, offset); offset += 2; part.m.b = bin.readF2dot14(data, offset); offset += 2; part.m.c = bin.readF2dot14(data, offset); offset += 2; part.m.d = bin.readF2dot14(data, offset); offset += 2; } } while (flags & MORE_COMPONENTS); if (flags & WE_HAVE_INSTRUCTIONS) { var numInstr = bin.readUshort(data, offset); offset += 2; gl.instr = []; for (var i = 0; i < numInstr; i++) { gl.instr.push(data[offset]); offset++; } } } return gl; } }; Typr["T"].head = { parseTab: function(data, offset, length) { var bin = Typr["B"]; var obj = {}; bin.readFixed(data, offset); offset += 4; obj["fontRevision"] = bin.readFixed(data, offset); offset += 4; bin.readUint(data, offset); offset += 4; bin.readUint(data, offset); offset += 4; obj["flags"] = bin.readUshort(data, offset); offset += 2; obj["unitsPerEm"] = bin.readUshort(data, offset); offset += 2; obj["created"] = bin.readUint64(data, offset); offset += 8; obj["modified"] = bin.readUint64(data, offset); offset += 8; obj["xMin"] = bin.readShort(data, offset); offset += 2; obj["yMin"] = bin.readShort(data, offset); offset += 2; obj["xMax"] = bin.readShort(data, offset); offset += 2; obj["yMax"] = bin.readShort(data, offset); offset += 2; obj["macStyle"] = bin.readUshort(data, offset); offset += 2; obj["lowestRecPPEM"] = bin.readUshort(data, offset); offset += 2; obj["fontDirectionHint"] = bin.readShort(data, offset); offset += 2; obj["indexToLocFormat"] = bin.readShort(data, offset); offset += 2; obj["glyphDataFormat"] = bin.readShort(data, offset); offset += 2; return obj; } }; Typr["T"].hhea = { parseTab: function(data, offset, length) { var bin = Typr["B"]; var obj = {}; bin.readFixed(data, offset); offset += 4; var keys = [ "ascender", "descender", "lineGap", "advanceWidthMax", "minLeftSideBearing", "minRightSideBearing", "xMaxExtent", "caretSlopeRise", "caretSlopeRun", "caretOffset", "res0", "res1", "res2", "res3", "metricDataFormat", "numberOfHMetrics" ]; for (var i = 0; i < keys.length; i++) { var key = keys[i]; var func = key == "advanceWidthMax" || key == "numberOfHMetrics" ? bin.readUshort : bin.readShort; obj[key] = func(data, offset + i * 2); } return obj; } }; Typr["T"].hmtx = { parseTab: function(data, offset, length, font) { var bin = Typr["B"]; var aWidth = []; var lsBearing = []; var nG = font["maxp"]["numGlyphs"], nH = font["hhea"]["numberOfHMetrics"]; var aw = 0, lsb = 0, i = 0; while (i < nH) { aw = bin.readUshort(data, offset + (i << 2)); lsb = bin.readShort(data, offset + (i << 2) + 2); aWidth.push(aw); lsBearing.push(lsb); i++; } while (i < nG) { aWidth.push(aw); lsBearing.push(lsb); i++; } return { aWidth: aWidth, lsBearing: lsBearing }; } }; Typr["T"].kern = { parseTab: function(data, offset, length, font) { var bin = Typr["B"], kern = Typr["T"].kern; var version = bin.readUshort(data, offset); if (version == 1) return kern.parseV1(data, offset, length, font); var nTables = bin.readUshort(data, offset + 2); offset += 4; var map = { glyph1: [], rval: [] }; for (var i = 0; i < nTables; i++) { offset += 2; var length = bin.readUshort(data, offset); offset += 2; var coverage = bin.readUshort(data, offset); offset += 2; var format = coverage >>> 8; format &= 15; if (format == 0) offset = kern.readFormat0(data, offset, map); } return map; }, parseV1: function(data, offset, length, font) { var bin = Typr["B"], kern = Typr["T"].kern; bin.readFixed(data, offset); var nTables = bin.readUint(data, offset + 4); offset += 8; var map = { glyph1: [], rval: [] }; for (var i = 0; i < nTables; i++) { bin.readUint(data, offset); offset += 4; var coverage = bin.readUshort(data, offset); offset += 2; bin.readUshort(data, offset); offset += 2; var format = coverage & 255; if (format == 0) offset = kern.readFormat0(data, offset, map); } return map; }, readFormat0: function(data, offset, map) { var bin = Typr["B"], rUs = bin.readUshort; var pleft = -1; var nPairs = rUs(data, offset); rUs(data, offset + 2); rUs(data, offset + 4); rUs(data, offset + 6); offset += 8; for (var j = 0; j < nPairs; j++) { var left = rUs(data, offset); offset += 2; var right = rUs(data, offset); offset += 2; var value = bin.readShort(data, offset); offset += 2; if (left != pleft) { map.glyph1.push(left); map.rval.push({ glyph2: [], vals: [] }); } var rval = map.rval[map.rval.length - 1]; rval.glyph2.push(right); rval.vals.push(value); pleft = left; } return offset; } }; Typr["T"].loca = { parseTab: function(data, offset, length, font) { var bin = Typr["B"]; var obj = []; var ver = font["head"]["indexToLocFormat"]; var len = font["maxp"]["numGlyphs"] + 1; if (ver == 0) for (var i = 0; i < len; i++) obj.push(bin.readUshort(data, offset + (i << 1)) << 1); if (ver == 1) for (var i = 0; i < len; i++) obj.push(bin.readUint(data, offset + (i << 2))); return obj; } }; Typr["T"].maxp = { parseTab: function(data, offset, length) { var bin = Typr["B"], rU = bin.readUshort; var obj = {}; bin.readUint(data, offset); offset += 4; obj["numGlyphs"] = rU(data, offset); offset += 2; return obj; } }; Typr["T"].name = { parseTab: function(data, offset, length) { var bin = Typr["B"]; var obj = {}; bin.readUshort(data, offset); offset += 2; var count = bin.readUshort(data, offset); offset += 2; bin.readUshort(data, offset); offset += 2; var names = [ "copyright", "fontFamily", "fontSubfamily", "ID", "fullName", "version", "postScriptName", "trademark", "manufacturer", "designer", "description", "urlVendor", "urlDesigner", "licence", "licenceURL", "---", "typoFamilyName", "typoSubfamilyName", "compatibleFull", "sampleText", "postScriptCID", "wwsFamilyName", "wwsSubfamilyName", "lightPalette", "darkPalette" ]; var offset0 = offset; var rU = bin.readUshort; for (var i = 0; i < count; i++) { var platformID = rU(data, offset); offset += 2; var encodingID = rU(data, offset); offset += 2; var languageID = rU(data, offset); offset += 2; var nameID = rU(data, offset); offset += 2; var slen = rU(data, offset); offset += 2; var noffset = rU(data, offset); offset += 2; var soff = offset0 + count * 12 + noffset; var str; if (platformID == 0) str = bin.readUnicode(data, soff, slen / 2); else if (platformID == 3 && encodingID == 0) str = bin.readUnicode(data, soff, slen / 2); else if (encodingID == 0) str = bin.readASCII(data, soff, slen); else if (encodingID == 1) str = bin.readUnicode(data, soff, slen / 2); else if (encodingID == 3) str = bin.readUnicode(data, soff, slen / 2); else if (encodingID == 4) str = bin.readUnicode(data, soff, slen / 2); else if (encodingID == 10) str = bin.readUnicode(data, soff, slen / 2); else if (platformID == 1) { str = bin.readASCII(data, soff, slen); console.log("reading unknown MAC encoding " + encodingID + " as ASCII"); } else { console.log("unknown encoding " + encodingID + ", platformID: " + platformID); str = bin.readASCII(data, soff, slen); } var tid = "p" + platformID + "," + languageID.toString(16); if (obj[tid] == null) obj[tid] = {}; obj[tid][names[nameID]] = str; obj[tid]["_lang"] = languageID; } var psn = "postScriptName"; for (var p in obj) if (obj[p][psn] != null && obj[p]["_lang"] == 1033) return obj[p]; for (var p in obj) if (obj[p][psn] != null && obj[p]["_lang"] == 0) return obj[p]; for (var p in obj) if (obj[p][psn] != null && obj[p]["_lang"] == 3084) return obj[p]; for (var p in obj) if (obj[p][psn] != null) return obj[p]; var out; for (var p in obj) { out = obj[p]; break; } console.log("returning name table with languageID " + out._lang); if (out[psn] == null && out["ID"] != null) out[psn] = out["ID"]; return out; } }; Typr["T"].OS2 = { parseTab: function(data, offset, length) { var bin = Typr["B"]; var ver = bin.readUshort(data, offset); offset += 2; var OS2 = Typr["T"].OS2; var obj = {}; if (ver == 0) OS2.version0(data, offset, obj); else if (ver == 1) OS2.version1(data, offset, obj); else if (ver == 2 || ver == 3 || ver == 4) OS2.version2(data, offset, obj); else if (ver == 5) OS2.version5(data, offset, obj); else throw "unknown OS/2 table version: " + ver; return obj; }, version0: function(data, offset, obj) { var bin = Typr["B"]; obj["xAvgCharWidth"] = bin.readShort(data, offset); offset += 2; obj["usWeightClass"] = bin.readUshort(data, offset); offset += 2; obj["usWidthClass"] = bin.readUshort(data, offset); offset += 2; obj["fsType"] = bin.readUshort(data, offset); offset += 2; obj["ySubscriptXSize"] = bin.readShort(data, offset); offset += 2; obj["ySubscriptYSize"] = bin.readShort(data, offset); offset += 2; obj["ySubscriptXOffset"] = bin.readShort(data, offset); offset += 2; obj["ySubscriptYOffset"] = bin.readShort(data, offset); offset += 2; obj["ySuperscriptXSize"] = bin.readShort(data, offset); offset += 2; obj["ySuperscriptYSize"] = bin.readShort(data, offset); offset += 2; obj["ySuperscriptXOffset"] = bin.readShort(data, offset); offset += 2; obj["ySuperscriptYOffset"] = bin.readShort(data, offset); offset += 2; obj["yStrikeoutSize"] = bin.readShort(data, offset); offset += 2; obj["yStrikeoutPosition"] = bin.readShort(data, offset); offset += 2; obj["sFamilyClass"] = bin.readShort(data, offset); offset += 2; obj["panose"] = bin.readBytes(data, offset, 10); offset += 10; obj["ulUnicodeRange1"] = bin.readUint(data, offset); offset += 4; obj["ulUnicodeRange2"] = bin.readUint(data, offset); offset += 4; obj["ulUnicodeRange3"] = bin.readUint(data, offset); offset += 4; obj["ulUnicodeRange4"] = bin.readUint(data, offset); offset += 4; obj["achVendID"] = bin.readASCII(data, offset, 4); offset += 4; obj["fsSelection"] = bin.readUshort(data, offset); offset += 2; obj["usFirstCharIndex"] = bin.readUshort(data, offset); offset += 2; obj["usLastCharIndex"] = bin.readUshort(data, offset); offset += 2; obj["sTypoAscender"] = bin.readShort(data, offset); offset += 2; obj["sTypoDescender"] = bin.readShort(data, offset); offset += 2; obj["sTypoLineGap"] = bin.readShort(data, offset); offset += 2; obj["usWinAscent"] = bin.readUshort(data, offset); offset += 2; obj["usWinDescent"] = bin.readUshort(data, offset); offset += 2; return offset; }, version1: function(data, offset, obj) { var bin = Typr["B"]; offset = Typr["T"].OS2.version0(data, offset, obj); obj["ulCodePageRange1"] = bin.readUint(data, offset); offset += 4; obj["ulCodePageRange2"] = bin.readUint(data, offset); offset += 4; return offset; }, version2: function(data, offset, obj) { var bin = Typr["B"], rU = bin.readUshort; offset = Typr["T"].OS2.version1(data, offset, obj); obj["sxHeight"] = bin.readShort(data, offset); offset += 2; obj["sCapHeight"] = bin.readShort(data, offset); offset += 2; obj["usDefault"] = rU(data, offset); offset += 2; obj["usBreak"] = rU(data, offset); offset += 2; obj["usMaxContext"] = rU(data, offset); offset += 2; return offset; }, version5: function(data, offset, obj) { var rU = Typr["B"].readUshort; offset = Typr["T"].OS2.version2(data, offset, obj); obj["usLowerOpticalPointSize"] = rU(data, offset); offset += 2; obj["usUpperOpticalPointSize"] = rU(data, offset); offset += 2; return offset; } }; Typr["T"].post = { parseTab: function(data, offset, length) { var bin = Typr["B"]; var obj = {}; obj["version"] = bin.readFixed(data, offset); offset += 4; obj["italicAngle"] = bin.readFixed(data, offset); offset += 4; obj["underlinePosition"] = bin.readShort(data, offset); offset += 2; obj["underlineThickness"] = bin.readShort(data, offset); offset += 2; return obj; } }; Typr["T"].SVG = { parseTab: function(data, offset, length) { var bin = Typr["B"]; var obj = { entries: [] }; var offset0 = offset; bin.readUshort(data, offset); offset += 2; var svgDocIndexOffset = bin.readUint(data, offset); offset += 4; bin.readUint(data, offset); offset += 4; offset = svgDocIndexOffset + offset0; var numEntries = bin.readUshort(data, offset); offset += 2; for (var i = 0; i < numEntries; i++) { var startGlyphID = bin.readUshort(data, offset); offset += 2; var endGlyphID = bin.readUshort(data, offset); offset += 2; var svgDocOffset = bin.readUint(data, offset); offset += 4; var svgDocLength = bin.readUint(data, offset); offset += 4; var sbuf = new Uint8Array(data.buffer, offset0 + svgDocOffset + svgDocIndexOffset, svgDocLength); var svg = bin.readUTF8(sbuf, 0, sbuf.length); for (var f = startGlyphID; f <= endGlyphID; f++) { obj.entries[f] = svg; } } return obj; } }; Typr["U"] = { shape: function(font, str, ltr) { var getGlyphPosition = function(font, gls, i1, ltr) { var g1 = gls[i1], g2 = gls[i1 + 1], kern = font["kern"]; if (kern) { var ind1 = kern.glyph1.indexOf(g1); if (ind1 != -1) { var ind2 = kern.rval[ind1].glyph2.indexOf(g2); if (ind2 != -1) return [ 0, 0, kern.rval[ind1].vals[ind2], 0 ]; } } return [ 0, 0, 0, 0 ]; }; var gls = []; for (var i = 0; i < str.length; i++) { var cc = str.codePointAt(i); if (cc > 65535) i++; gls.push(Typr["U"]["codeToGlyph"](font, cc)); } var shape = []; for (var i = 0; i < gls.length; i++) { var padj = getGlyphPosition(font, gls, i); var gid = gls[i]; var ax = font["hmtx"].aWidth[gid] + padj[2]; shape.push({ g: gid, cl: i, dx: 0, dy: 0, ax: ax, ay: 0 }); } return shape; }, shapeToPath: function(font, shape, clr) { var tpath = { cmds: [], crds: [] }; var x = 0, y = 0; for (var i = 0; i < shape.length; i++) { var it = shape[i]; var path = Typr["U"]["glyphToPath"](font, it["g"]), crds = path["crds"]; for (var j = 0; j < crds.length; j += 2) { tpath.crds.push(crds[j] + x + it["dx"]); tpath.crds.push(crds[j + 1] + y + it["dy"]); } if (clr) tpath.cmds.push(clr); for (var j = 0; j < path["cmds"].length; j++) tpath.cmds.push(path["cmds"][j]); var clen = tpath.cmds.length; if (clr) if (clen != 0 && tpath.cmds[clen - 1] != "X") tpath.cmds.push("X"); x += it["ax"]; y += it["ay"]; } return { cmds: tpath.cmds, crds: tpath.crds }; }, codeToGlyph: function(font, code) { var cmap = font["cmap"]; var tind = -1, pps = [ "p3e10", "p0e4", "p3e1", "p1e0", "p0e3", "p0e1" ]; for (var i = 0; i < pps.length; i++) if (cmap.ids[pps[i]] != null) { tind = cmap.ids[pps[i]]; break; } if (tind == -1) throw "no familiar platform and encoding!"; var arrSearch = function(arr, k, v) { var l = 0, r = Math.floor(arr.length / k); while (l + 1 != r) { var mid = l + (r - l >>> 1); if (arr[mid * k] <= v) l = mid; else r = mid; } return l * k; }; var tab = cmap.tables[tind], fmt = tab.format, gid = -1; if (fmt == 0) { if (code >= tab.map.length) gid = 0; else gid = tab.map[code]; } else if (fmt == 4) { var sind = -1, ec = tab.endCount; if (code > ec[ec.length - 1]) sind = -1; else { sind = arrSearch(ec, 1, code); if (ec[sind] < code) sind++; } if (sind == -1) gid = 0; else if (code < tab.startCount[sind]) gid = 0; else { var gli = 0; if (tab.idRangeOffset[sind] != 0) gli = tab.glyphIdArray[code - tab.startCount[sind] + (tab.idRangeOffset[sind] >> 1) - (tab.idRangeOffset.length - sind)]; else gli = code + tab.idDelta[sind]; gid = gli & 65535; } } else if (fmt == 6) { var off = code - tab.firstCode, arr = tab.glyphIdArray; if (off < 0 || off >= arr.length) gid = 0; else gid = arr[off]; } else if (fmt == 12) { var grp = tab.groups; if (code > grp[grp.length - 2]) gid = 0; else { var i = arrSearch(grp, 3, code); if (grp[i] <= code && code <= grp[i + 1]) { gid = grp[i + 2] + (code - grp[i]); } if (gid == -1) gid = 0; } } else throw "unknown cmap table format " + tab.format; var SVG = font["SVG "], loca = font["loca"]; if (gid != 0 && font["CFF "] == null && (SVG == null || SVG.entries[gid] == null) && loca[gid] == loca[gid + 1] && [ 9, 10, 11, 12, 13, 32, 133, 160, 5760, 8232, 8233, 8239, 12288, 6158, 8203, 8204, 8205, 8288, 65279 ].indexOf(code) == -1 && !(8192 <= code && code <= 8202)) gid = 0; return gid; }, glyphToPath: function(font, gid) { var path = { cmds: [], crds: [] }; var SVG = font["SVG "], CFF = font["CFF "]; var U = Typr["U"]; if (SVG && SVG.entries[gid]) { var p = SVG.entries[gid]; if (p != null) { if (typeof p == "string") { p = U["SVG"].toPath(p); SVG.entries[gid] = p; } path = p; } } else if (CFF) { var pdct = CFF["Private"]; var state = { x: 0, y: 0, stack: [], nStems: 0, haveWidth: false, width: pdct ? pdct["defaultWidthX"] : 0, open: false }; if (CFF["ROS"]) { var gi = 0; while (CFF["FDSelect"][gi + 2] <= gid) gi += 2; pdct = CFF["FDArray"][CFF["FDSelect"][gi + 1]]["Private"]; } U["_drawCFF"](CFF["CharStrings"][gid], state, CFF, pdct, path); } else if (font["glyf"]) { U["_drawGlyf"](gid, font, path); } return { cmds: path.cmds, crds: path.crds }; }, _drawGlyf: function(gid, font, path) { var gl = font["glyf"][gid]; if (gl == null) gl = font["glyf"][gid] = Typr["T"].glyf._parseGlyf(font, gid); if (gl != null) { if (gl.noc > -1) Typr["U"]["_simpleGlyph"](gl, path); else Typr["U"]["_compoGlyph"](gl, font, path); } }, _simpleGlyph: function(gl, p) { var P = Typr["U"]["P"]; for (var c = 0; c < gl.noc; c++) { var i0 = c == 0 ? 0 : gl.endPts[c - 1] + 1; var il = gl.endPts[c]; for (var i = i0; i <= il; i++) { var pr = i == i0 ? il : i - 1; var nx = i == il ? i0 : i + 1; var onCurve = gl.flags[i] & 1; var prOnCurve = gl.flags[pr] & 1; var nxOnCurve = gl.flags[nx] & 1; var x = gl.xs[i], y = gl.ys[i]; if (i == i0) { if (onCurve) { if (prOnCurve) P.MoveTo(p, gl.xs[pr], gl.ys[pr]); else { P.MoveTo(p, x, y); continue; } } else { if (prOnCurve) P.MoveTo(p, gl.xs[pr], gl.ys[pr]); else P.MoveTo(p, Math.floor((gl.xs[pr] + x) * .5), Math.floor((gl.ys[pr] + y) * .5)); } } if (onCurve) { if (prOnCurve) P.LineTo(p, x, y); } else { if (nxOnCurve) P.qCurveTo(p, x, y, gl.xs[nx], gl.ys[nx]); else P.qCurveTo(p, x, y, Math.floor((x + gl.xs[nx]) * .5), Math.floor((y + gl.ys[nx]) * .5)); } } P.ClosePath(p); } }, _compoGlyph: function(gl, font, p) { for (var j = 0; j < gl.parts.length; j++) { var path = { cmds: [], crds: [] }; var prt = gl.parts[j]; Typr["U"]["_drawGlyf"](prt.glyphIndex, font, path); var m = prt.m; for (var i = 0; i < path.crds.length; i += 2) { var x = path.crds[i], y = path.crds[i + 1]; p.crds.push(x * m.a + y * m.b + m.tx); p.crds.push(x * m.c + y * m.d + m.ty); } for (var i = 0; i < path.cmds.length; i++) p.cmds.push(path.cmds[i]); } }, pathToSVG: function(path, prec) { var cmds = path["cmds"], crds = path["crds"]; if (prec == null) prec = 5; var out = [], co = 0, lmap = { M: 2, L: 2, Q: 4, C: 6 }; for (var i = 0; i < cmds.length; i++) { var cmd = cmds[i], cn = co + (lmap[cmd] ? lmap[cmd] : 0); out.push(cmd); while (co < cn) { var c = crds[co++]; out.push(parseFloat(c.toFixed(prec)) + (co == cn ? "" : " ")); } } return out.join(""); }, SVGToPath: function(d) { var pth = { cmds: [], crds: [] }; Typr["U"]["SVG"].svgToPath(d, pth); return { cmds: pth.cmds, crds: pth.crds }; }, pathToContext: function(path, ctx) { var c = 0, cmds = path["cmds"], crds = path["crds"]; for (var j = 0; j < cmds.length; j++) { var cmd = cmds[j]; if (cmd == "M") { ctx.moveTo(crds[c], crds[c + 1]); c += 2; } else if (cmd == "L") { ctx.lineTo(crds[c], crds[c + 1]); c += 2; } else if (cmd == "C") { ctx.bezierCurveTo(crds[c], crds[c + 1], crds[c + 2], crds[c + 3], crds[c + 4], crds[c + 5]); c += 6; } else if (cmd == "Q") { ctx.quadraticCurveTo(crds[c], crds[c + 1], crds[c + 2], crds[c + 3]); c += 4; } else if (cmd.charAt(0) == "#") { ctx.beginPath(); ctx.fillStyle = cmd; } else if (cmd == "Z") { ctx.closePath(); } else if (cmd == "X") { ctx.fill(); } } }, P: { MoveTo: function(p, x, y) { p.cmds.push("M"); p.crds.push(x, y); }, LineTo: function(p, x, y) { p.cmds.push("L"); p.crds.push(x, y); }, CurveTo: function(p, a, b, c, d, e, f) { p.cmds.push("C"); p.crds.push(a, b, c, d, e, f); }, qCurveTo: function(p, a, b, c, d) { p.cmds.push("Q"); p.crds.push(a, b, c, d); }, ClosePath: function(p) { p.cmds.push("Z"); } }, _drawCFF: function(cmds, state, font, pdct, p) { var stack = state.stack; var nStems = state.nStems, haveWidth = state.haveWidth, width = state.width, open = state.open; var i = 0; var x = state.x, y = state.y, c1x = 0, c1y = 0, c2x = 0, c2y = 0, c3x = 0, c3y = 0, c4x = 0, c4y = 0, jpx = 0, jpy = 0; var CFF = Typr["T"].CFF, P = Typr["U"]["P"]; var nominalWidthX = pdct["nominalWidthX"]; var o = { val: 0, size: 0 }; while (i < cmds.length) { CFF.getCharString(cmds, i, o); var v = o.val; i += o.size; if (v == "o1" || v == "o18") { var hasWidthArg; hasWidthArg = stack.length % 2 !== 0; if (hasWidthArg && !haveWidth) { width = stack.shift() + nominalWidthX; } nStems += stack.length >> 1; stack.length = 0; haveWidth = true; } else if (v == "o3" || v == "o23") { var hasWidthArg; hasWidthArg = stack.length % 2 !== 0; if (hasWidthArg && !haveWidth) { width = stack.shift() + nominalWidthX; } nStems += stack.length >> 1; stack.length = 0; haveWidth = true; } else if (v == "o4") { if (stack.length > 1 && !haveWidth) { width = stack.shift() + nominalWidthX; haveWidth = true; } if (open) P.ClosePath(p); y += stack.pop(); P.MoveTo(p, x, y); open = true; } else if (v == "o5") { while (stack.length > 0) { x += stack.shift(); y += stack.shift(); P.LineTo(p, x, y); } } else if (v == "o6" || v == "o7") { var count = stack.length; var isX = v == "o6"; for (var j = 0; j < count; j++) { var sval = stack.shift(); if (isX) x += sval; else y += sval; isX = !isX; P.LineTo(p, x, y); } } else if (v == "o8" || v == "o24") { var count = stack.length; var index = 0; while (index + 6 <= count) { c1x = x + stack.shift(); c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); x = c2x + stack.shift(); y = c2y + stack.shift(); P.CurveTo(p, c1x, c1y, c2x, c2y, x, y); index += 6; } if (v == "o24") { x += stack.shift(); y += stack.shift(); P.LineTo(p, x, y); } } else if (v == "o11") break; else if (v == "o1234" || v == "o1235" || v == "o1236" || v == "o1237") { if (v == "o1234") { c1x = x + stack.shift(); c1y = y; c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); jpx = c2x + stack.shift(); jpy = c2y; c3x = jpx + stack.shift(); c3y = c2y; c4x = c3x + stack.shift(); c4y = y; x = c4x + stack.shift(); P.CurveTo(p, c1x, c1y, c2x, c2y, jpx, jpy); P.CurveTo(p, c3x, c3y, c4x, c4y, x, y); } if (v == "o1235") { c1x = x + stack.shift(); c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); jpx = c2x + stack.shift(); jpy = c2y + stack.shift(); c3x = jpx + stack.shift(); c3y = jpy + stack.shift(); c4x = c3x + stack.shift(); c4y = c3y + stack.shift(); x = c4x + stack.shift(); y = c4y + stack.shift(); stack.shift(); P.CurveTo(p, c1x, c1y, c2x, c2y, jpx, jpy); P.CurveTo(p, c3x, c3y, c4x, c4y, x, y); } if (v == "o1236") { c1x = x + stack.shift(); c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); jpx = c2x + stack.shift(); jpy = c2y; c3x = jpx + stack.shift(); c3y = c2y; c4x = c3x + stack.shift(); c4y = c3y + stack.shift(); x = c4x + stack.shift(); P.CurveTo(p, c1x, c1y, c2x, c2y, jpx, jpy); P.CurveTo(p, c3x, c3y, c4x, c4y, x, y); } if (v == "o1237") { c1x = x + stack.shift(); c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); jpx = c2x + stack.shift(); jpy = c2y + stack.shift(); c3x = jpx + stack.shift(); c3y = jpy + stack.shift(); c4x = c3x + stack.shift(); c4y = c3y + stack.shift(); if (Math.abs(c4x - x) > Math.abs(c4y - y)) { x = c4x + stack.shift(); } else { y = c4y + stack.shift(); } P.CurveTo(p, c1x, c1y, c2x, c2y, jpx, jpy); P.CurveTo(p, c3x, c3y, c4x, c4y, x, y); } } else if (v == "o14") { if (stack.length > 0 && !haveWidth) { width = stack.shift() + font["nominalWidthX"]; haveWidth = true; } if (stack.length == 4) { var adx = stack.shift(); var ady = stack.shift(); var bchar = stack.shift(); var achar = stack.shift(); var bind = CFF.glyphBySE(font, bchar); var aind = CFF.glyphBySE(font, achar); Typr["U"]["_drawCFF"](font["CharStrings"][bind], state, font, pdct, p); state.x = adx; state.y = ady; Typr["U"]["_drawCFF"](font["CharStrings"][aind], state, font, pdct, p); } if (open) { P.ClosePath(p); open = false; } } else if (v == "o19" || v == "o20") { var hasWidthArg; hasWidthArg = stack.length % 2 !== 0; if (hasWidthArg && !haveWidth) { width = stack.shift() + nominalWidthX; } nStems += stack.length >> 1; stack.length = 0; haveWidth = true; i += nStems + 7 >> 3; } else if (v == "o21") { if (stack.length > 2 && !haveWidth) { width = stack.shift() + nominalWidthX; haveWidth = true; } y += stack.pop(); x += stack.pop(); if (open) P.ClosePath(p); P.MoveTo(p, x, y); open = true; } else if (v == "o22") { if (stack.length > 1 && !haveWidth) { width = stack.shift() + nominalWidthX; haveWidth = true; } x += stack.pop(); if (open) P.ClosePath(p); P.MoveTo(p, x, y); open = true; } else if (v == "o25") { while (stack.length > 6) { x += stack.shift(); y += stack.shift(); P.LineTo(p, x, y); } c1x = x + stack.shift(); c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); x = c2x + stack.shift(); y = c2y + stack.shift(); P.CurveTo(p, c1x, c1y, c2x, c2y, x, y); } else if (v == "o26") { if (stack.length % 2) { x += stack.shift(); } while (stack.length > 0) { c1x = x; c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); x = c2x; y = c2y + stack.shift(); P.CurveTo(p, c1x, c1y, c2x, c2y, x, y); } } else if (v == "o27") { if (stack.length % 2) { y += stack.shift(); } while (stack.length > 0) { c1x = x + stack.shift(); c1y = y; c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); x = c2x + stack.shift(); y = c2y; P.CurveTo(p, c1x, c1y, c2x, c2y, x, y); } } else if (v == "o10" || v == "o29") { var obj = v == "o10" ? pdct : font; if (stack.length == 0) { console.log("error: empty stack"); } else { var ind = stack.pop(); var subr = obj["Subrs"][ind + obj["Bias"]]; state.x = x; state.y = y; state.nStems = nStems; state.haveWidth = haveWidth; state.width = width; state.open = open; Typr["U"]["_drawCFF"](subr, state, font, pdct, p); x = state.x; y = state.y; nStems = state.nStems; haveWidth = state.haveWidth; width = state.width; open = state.open; } } else if (v == "o30" || v == "o31") { var count, count1 = stack.length; var index = 0; var alternate = v == "o31"; count = count1 & ~2; index += count1 - count; while (index < count) { if (alternate) { c1x = x + stack.shift(); c1y = y; c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); y = c2y + stack.shift(); if (count - index == 5) { x = c2x + stack.shift(); index++; } else x = c2x; alternate = false; } else { c1x = x; c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); x = c2x + stack.shift(); if (count - index == 5) { y = c2y + stack.shift(); index++; } else y = c2y; alternate = true; } P.CurveTo(p, c1x, c1y, c2x, c2y, x, y); index += 4; } } else if ((v + "").charAt(0) == "o") { console.log("Unknown operation: " + v, cmds); throw v; } else stack.push(v); } state.x = x; state.y = y; state.nStems = nStems; state.haveWidth = haveWidth; state.width = width; state.open = open; }, SVG: function() { var M = { getScale: function(m) { return Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2])); }, translate: function(m, x, y) { M.concat(m, [ 1, 0, 0, 1, x, y ]); }, rotate: function(m, a) { M.concat(m, [ Math.cos(a), -Math.sin(a), Math.sin(a), Math.cos(a), 0, 0 ]); }, scale: function(m, x, y) { M.concat(m, [ x, 0, 0, y, 0, 0 ]); }, concat: function(m, w) { var a = m[0], b = m[1], c = m[2], d = m[3], tx = m[4], ty = m[5]; m[0] = a * w[0] + b * w[2]; m[1] = a * w[1] + b * w[3]; m[2] = c * w[0] + d * w[2]; m[3] = c * w[1] + d * w[3]; m[4] = tx * w[0] + ty * w[2] + w[4]; m[5] = tx * w[1] + ty * w[3] + w[5]; }, invert: function(m) { var a = m[0], b = m[1], c = m[2], d = m[3], tx = m[4], ty = m[5], adbc = a * d - b * c; m[0] = d / adbc; m[1] = -b / adbc; m[2] = -c / adbc; m[3] = a / adbc; m[4] = (c * ty - d * tx) / adbc; m[5] = (b * tx - a * ty) / adbc; }, multPoint: function(m, p) { var x = p[0], y = p[1]; return [ x * m[0] + y * m[2] + m[4], x * m[1] + y * m[3] + m[5] ]; }, multArray: function(m, a) { for (var i = 0; i < a.length; i += 2) { var x = a[i], y = a[i + 1]; a[i] = x * m[0] + y * m[2] + m[4]; a[i + 1] = x * m[1] + y * m[3] + m[5]; } } }; function _bracketSplit(str, lbr, rbr) { var out = [], pos = 0, ci = 0, lvl = 0; while (true) { var li = str.indexOf(lbr, ci); var ri = str.indexOf(rbr, ci); if (li == -1 && ri == -1) break; if (ri == -1 || li != -1 && li < ri) { if (lvl == 0) { out.push(str.slice(pos, li).trim()); pos = li + 1; } lvl++; ci = li + 1; } else if (li == -1 || ri != -1 && ri < li) { lvl--; if (lvl == 0) { out.push(str.slice(pos, ri).trim()); pos = ri + 1; } ci = ri + 1; } } return out; } function cssMap(str) { var pts = _bracketSplit(str, "{", "}"); var css = {}; for (var i = 0; i < pts.length; i += 2) { var cn = pts[i].split(","); for (var j = 0; j < cn.length; j++) { var cnj = cn[j].trim(); if (css[cnj] == null) css[cnj] = ""; css[cnj] += pts[i + 1]; } } return css; } function readTrnf(trna) { var pts = _bracketSplit(trna, "(", ")"); var m = [ 1, 0, 0, 1, 0, 0 ]; for (var i = 0; i < pts.length; i += 2) { var om = m; m = _readTrnsAttr(pts[i], pts[i + 1]); M.concat(m, om); } return m; } function _readTrnsAttr(fnc, vls) { var m = [ 1, 0, 0, 1, 0, 0 ], gotSep = true; for (var i = 0; i < vls.length; i++) { var ch = vls.charAt(i); if (ch == "," || ch == " ") gotSep = true; else if (ch == ".") { if (!gotSep) { vls = vls.slice(0, i) + "," + vls.slice(i); i++; } gotSep = false; } else if (ch == "-" && i > 0 && vls[i - 1] != "e") { vls = vls.slice(0, i) + " " + vls.slice(i); i++; gotSep = true; } } vls = vls.split(/\s*[\s,]\s*/).map(parseFloat); if (fnc == "translate") { if (vls.length == 1) M.translate(m, vls[0], 0); else M.translate(m, vls[0], vls[1]); } else if (fnc == "scale") { if (vls.length == 1) M.scale(m, vls[0], vls[0]); else M.scale(m, vls[0], vls[1]); } else if (fnc == "rotate") { var tx = 0, ty = 0; if (vls.length != 1) { tx = vls[1]; ty = vls[2]; } M.translate(m, -tx, -ty); M.rotate(m, -Math.PI * vls[0] / 180); M.translate(m, tx, ty); } else if (fnc == "matrix") m = vls; else console.log("unknown transform: ", fnc); return m; } function toPath(str) { var pth = { cmds: [], crds: [] }; if (str == null) return pth; var prsr = new DOMParser(); var doc = prsr["parseFromString"](str, "image/svg+xml"); var svg = doc.getElementsByTagName("svg")[0]; var vb = svg.getAttribute("viewBox"); if (vb) vb = vb.trim().split(" ").map(parseFloat); else vb = [ 0, 0, 1e3, 1e3 ]; _toPath(svg.children, pth); for (var i = 0; i < pth.crds.length; i += 2) { var x = pth.crds[i], y = pth.crds[i + 1]; x -= vb[0]; y -= vb[1]; y = -y; pth.crds[i] = x; pth.crds[i + 1] = y; } return pth; } function _toPath(nds, pth, fill) { for (var ni = 0; ni < nds.length; ni++) { var nd = nds[ni], tn = nd.tagName; var cfl = nd.getAttribute("fill"); if (cfl == null) cfl = fill; if (tn == "g") { var tp = { crds: [], cmds: [] }; _toPath(nd.children, tp, cfl); var trf = nd.getAttribute("transform"); if (trf) { var m = readTrnf(trf); M.multArray(m, tp.crds); } pth.crds = pth.crds.concat(tp.crds); pth.cmds = pth.cmds.concat(tp.cmds); } else if (tn == "path" || tn == "circle" || tn == "ellipse") { pth.cmds.push(cfl ? cfl : "#000000"); var d; if (tn == "path") d = nd.getAttribute("d"); if (tn == "circle" || tn == "ellipse") { var vls = [ 0, 0, 0, 0 ], nms = [ "cx", "cy", "rx", "ry", "r" ]; for (var i = 0; i < 5; i++) { var V = nd.getAttribute(nms[i]); if (V) { V = parseFloat(V); if (i < 4) vls[i] = V; else vls[2] = vls[3] = V; } } var cx = vls[0], cy = vls[1], rx = vls[2], ry = vls[3]; d = [ "M", cx - rx, cy, "a", rx, ry, 0, 1, 0, rx * 2, 0, "a", rx, ry, 0, 1, 0, -rx * 2, 0 ].join(" "); } svgToPath(d, pth); pth.cmds.push("X"); } else if (tn == "defs"); else console.log(tn, nd); } } function _tokens(d) { var ts = [], off = 0, rn = false, cn = "", pc = ""; while (off < d.length) { var cc = d.charCodeAt(off), ch = d.charAt(off); off++; var isNum = 48 <= cc && cc <= 57 || ch == "." || ch == "-" || ch == "e" || ch == "E"; if (rn) { if (ch == "-" && pc != "e" || ch == "." && cn.indexOf(".") != -1) { ts.push(parseFloat(cn)); cn = ch; } else if (isNum) cn += ch; else { ts.push(parseFloat(cn)); if (ch != "," && ch != " ") ts.push(ch); rn = false; } } else { if (isNum) { cn = ch; rn = true; } else if (ch != "," && ch != " ") ts.push(ch); } pc = ch; } if (rn) ts.push(parseFloat(cn)); return ts; } function _reps(ts, off, ps) { var i = off; while (i < ts.length) { if (typeof ts[i] == "string") break; i += ps; } return (i - off) / ps; } function svgToPath(d, pth) { var ts = _tokens(d); var i = 0, x = 0, y = 0, ox = 0, oy = 0, oldo = pth.crds.length; var pc = { M: 2, L: 2, H: 1, V: 1, T: 2, S: 4, A: 7, Q: 4, C: 6 }; var cmds = pth.cmds, crds = pth.crds; while (i < ts.length) { var cmd = ts[i]; i++; var cmu = cmd.toUpperCase(); if (cmu == "Z") { cmds.push("Z"); x = ox; y = oy; } else { var ps = pc[cmu], reps = _reps(ts, i, ps); for (var j = 0; j < reps; j++) { if (j == 1 && cmu == "M") { cmd = cmd == cmu ? "L" : "l"; cmu = "L"; } var xi = 0, yi = 0; if (cmd != cmu) { xi = x; yi = y; } if (cmu == "M") { x = xi + ts[i++]; y = yi + ts[i++]; cmds.push("M"); crds.push(x, y); ox = x; oy = y; } else if (cmu == "L") { x = xi + ts[i++]; y = yi + ts[i++]; cmds.push("L"); crds.push(x, y); } else if (cmu == "H") { x = xi + ts[i++]; cmds.push("L"); crds.push(x, y); } else if (cmu == "V") { y = yi + ts[i++]; cmds.push("L"); crds.push(x, y); } else if (cmu == "Q") { var x1 = xi + ts[i++], y1 = yi + ts[i++], x2 = xi + ts[i++], y2 = yi + ts[i++]; cmds.push("Q"); crds.push(x1, y1, x2, y2); x = x2; y = y2; } else if (cmu == "T") { var co = Math.max(crds.length - 2, oldo); var x1 = x + x - crds[co], y1 = y + y - crds[co + 1]; var x2 = xi + ts[i++], y2 = yi + ts[i++]; cmds.push("Q"); crds.push(x1, y1, x2, y2); x = x2; y = y2; } else if (cmu == "C") { var x1 = xi + ts[i++], y1 = yi + ts[i++], x2 = xi + ts[i++], y2 = yi + ts[i++], x3 = xi + ts[i++], y3 = yi + ts[i++]; cmds.push("C"); crds.push(x1, y1, x2, y2, x3, y3); x = x3; y = y3; } else if (cmu == "S") { var co = Math.max(crds.length - (cmds[cmds.length - 1] == "C" ? 4 : 2), oldo); var x1 = x + x - crds[co], y1 = y + y - crds[co + 1]; var x2 = xi + ts[i++], y2 = yi + ts[i++], x3 = xi + ts[i++], y3 = yi + ts[i++]; cmds.push("C"); crds.push(x1, y1, x2, y2, x3, y3); x = x3; y = y3; } else if (cmu == "A") { var x1 = x, y1 = y; var rx = ts[i++], ry = ts[i++]; var phi = ts[i++] * (Math.PI / 180), fA = ts[i++], fS = ts[i++]; var x2 = xi + ts[i++], y2 = yi + ts[i++]; if (x2 == x && y2 == y && rx == 0 && ry == 0) continue; var hdx = (x1 - x2) / 2, hdy = (y1 - y2) / 2; var cosP = Math.cos(phi), sinP = Math.sin(phi); var x1A = cosP * hdx + sinP * hdy; var y1A = -sinP * hdx + cosP * hdy; var rxS = rx * rx, ryS = ry * ry; var x1AS = x1A * x1A, y1AS = y1A * y1A; var frc = (rxS * ryS - rxS * y1AS - ryS * x1AS) / (rxS * y1AS + ryS * x1AS); var coef = (fA != fS ? 1 : -1) * Math.sqrt(Math.max(frc, 0)); var cxA = coef * (rx * y1A) / ry; var cyA = -coef * (ry * x1A) / rx; var cx = cosP * cxA - sinP * cyA + (x1 + x2) / 2; var cy = sinP * cxA + cosP * cyA + (y1 + y2) / 2; var angl = function(ux, uy, vx, vy) { var lU = Math.sqrt(ux * ux + uy * uy), lV = Math.sqrt(vx * vx + vy * vy); var num = (ux * vx + uy * vy) / (lU * lV); return (ux * vy - uy * vx >= 0 ? 1 : -1) * Math.acos(Math.max(-1, Math.min(1, num))); }; var vX = (x1A - cxA) / rx, vY = (y1A - cyA) / ry; var theta1 = angl(1, 0, vX, vY); var dtheta = angl(vX, vY, (-x1A - cxA) / rx, (-y1A - cyA) / ry); dtheta = dtheta % (2 * Math.PI); var arc = function(gst, x, y, r, a0, a1, neg) { var rotate = function(m, a) { var si = Math.sin(a), co = Math.cos(a); var a = m[0], b = m[1], c = m[2], d = m[3]; m[0] = a * co + b * si; m[1] = -a * si + b * co; m[2] = c * co + d * si; m[3] = -c * si + d * co; }; var multArr = function(m, a) { for (var j = 0; j < a.length; j += 2) { var x = a[j], y = a[j + 1]; a[j] = m[0] * x + m[2] * y + m[4]; a[j + 1] = m[1] * x + m[3] * y + m[5]; } }; var concatA = function(a, b) { for (var j = 0; j < b.length; j++) a.push(b[j]); }; var concatP = function(p, r) { concatA(p.cmds, r.cmds); concatA(p.crds, r.crds); }; if (neg) while (a1 > a0) a1 -= 2 * Math.PI; else while (a1 < a0) a1 += 2 * Math.PI; var th = (a1 - a0) / 4; var x0 = Math.cos(th / 2), y0 = -Math.sin(th / 2); var x1 = (4 - x0) / 3, y1 = y0 == 0 ? y0 : (1 - x0) * (3 - x0) / (3 * y0); var x2 = x1, y2 = -y1; var x3 = x0, y3 = -y0; var ps = [ x1, y1, x2, y2, x3, y3 ]; var pth = { cmds: [ "C", "C", "C", "C" ], crds: ps.slice(0) }; var rot = [ 1, 0, 0, 1, 0, 0 ]; rotate(rot, -th); for (var j = 0; j < 3; j++) { multArr(rot, ps); concatA(pth.crds, ps); } rotate(rot, -a0 + th / 2); rot[0] *= r; rot[1] *= r; rot[2] *= r; rot[3] *= r; rot[4] = x; rot[5] = y; multArr(rot, pth.crds); multArr(gst.ctm, pth.crds); concatP(gst.pth, pth); }; var gst = { pth: pth, ctm: [ rx * cosP, rx * sinP, -ry * sinP, ry * cosP, cx, cy ] }; arc(gst, 0, 0, 1, theta1, theta1 + dtheta, fS == 0); x = x2; y = y2; } else console.log("Unknown SVG command " + cmd); } } } } return { cssMap: cssMap, readTrnf: readTrnf, svgToPath: svgToPath, toPath: toPath }; }(), initHB: function(hurl, resp) { var codeLength = function(code) { var len = 0; if ((code & 4294967295 - (1 << 7) + 1) == 0) { len = 1; } else if ((code & 4294967295 - (1 << 11) + 1) == 0) { len = 2; } else if ((code & 4294967295 - (1 << 16) + 1) == 0) { len = 3; } else if ((code & 4294967295 - (1 << 21) + 1) == 0) { len = 4; } return len; }; var te = new window["TextEncoder"]("utf8"); fetch(hurl).then(function(x) { return x["arrayBuffer"](); }).then(function(ab) { return WebAssembly["instantiate"](ab); }).then(function(res) { console.log("HB ready"); var exp = res["instance"]["exports"], mem = exp["memory"]; mem["grow"](700); var heapu8 = new Uint8Array(mem.buffer); var u32 = new Uint32Array(mem.buffer); var i32 = new Int32Array(mem.buffer); var __lastFnt, blob, blobPtr, face, font; Typr["U"]["shapeHB"] = function() { var toJson = function(ptr) { var length = exp["hb_buffer_get_length"](ptr); var result = []; var iPtr32 = exp["hb_buffer_get_glyph_infos"](ptr, 0) >>> 2; var pPtr32 = exp["hb_buffer_get_glyph_positions"](ptr, 0) >>> 2; for (var i = 0; i < length; ++i) { var a = iPtr32 + i * 5, b = pPtr32 + i * 5; result.push({ g: u32[a + 0], cl: u32[a + 2], ax: i32[b + 0], ay: i32[b + 1], dx: i32[b + 2], dy: i32[b + 3] }); } return result; }; return function(fnt, str, ltr) { var fdata = fnt["_data"], fn = fnt["name"]["postScriptName"]; if (__lastFnt != fn) { if (blob != null) { exp["hb_blob_destroy"](blob); exp["free"](blobPtr); exp["hb_face_destroy"](face); exp["hb_font_destroy"](font); } blobPtr = exp["malloc"](fdata.byteLength); heapu8.set(fdata, blobPtr); blob = exp["hb_blob_create"](blobPtr, fdata.byteLength, 2, 0, 0); face = exp["hb_face_create"](blob, 0); font = exp["hb_font_create"](face); __lastFnt = fn; } var buffer = exp["hb_buffer_create"](); var bytes = te["encode"](str); var len = bytes.length, strp = exp["malloc"](len); heapu8.set(bytes, strp); exp["hb_buffer_add_utf8"](buffer, strp, len, 0, len); exp["free"](strp); exp["hb_buffer_set_direction"](buffer, ltr ? 4 : 5); exp["hb_buffer_guess_segment_properties"](buffer); exp["hb_shape"](font, buffer, 0, 0); var json = toJson(buffer); exp["hb_buffer_destroy"](buffer); var arr = json.slice(0); if (!ltr) arr.reverse(); var ci = 0, bi = 0; for (var i = 1; i < arr.length; i++) { var gl = arr[i], cl = gl["cl"]; while (true) { var cpt = str.codePointAt(ci), cln = codeLength(cpt); if (bi + cln <= cl) { bi += cln; ci += cpt <= 65535 ? 1 : 2; } else break; } gl["cl"] = ci; } return json; }; }(); resp(); }); } }; const QQ_GROUP = [ "854137118" ]; var _self = unsafeWindow; var top = _self; var UE$1; var modelId = "modelId_xx"; const selfintv = setInterval(() => { if (unsafeWindow) { _self = unsafeWindow; top = _self; UE$1 = _self.UE; try { reportOnline(); String.prototype.replaceAll = function(s1, s2) { return this.replace(new RegExp(s1, "gm"), s2); }; while (top !== _self.top) { top = top.parent.document ? top.parent : _self.top; if (top.location.pathname === "/mycourse/studentstudy") break; } } catch (err) { top = _self; } clearInterval(selfintv); } }, GLOBAL.delay); function checkVersion() { function compare(v1 = "0", v2 = "0") { v1 = String(v1).split("."); v2 = String(v2).split("."); const minVersionLens = Math.min(v1.length, v2.length); let result = 0; for (let i = 0; i < minVersionLens; i++) { const curV1 = Number(v1[i]); const curV2 = Number(v2[i]); if (curV1 > curV2) { result = 1; break; } else if (curV1 < curV2) { result = -1; break; } } if (result === 0 && v1.length !== v2.length) { const v1BiggerThenv2 = v1.length > v2.length; const maxLensVersion = v1BiggerThenv2 ? v1 : v2; for (let i = minVersionLens; i < maxLensVersion.length; i++) { const curVersion = Number(maxLensVersion[i]); if (curVersion > 0) { v1BiggerThenv2 ? result = 1 : result = -1; break; } } } return result; } GM_xmlhttpRequest({ method: "GET", url: "https://greasyfork.org/en/scripts/451356.json", timeout: GLOBAL.timeout, onload: function(r) { const obj = JSON.parse(r.responseText); if (obj.name === GM_info.script.name && compare(obj.version, GM_info.script.version) === 1 && new Date(obj.code_updated_at).getTime() + 1e3 * 60 * 60 * 2 < new Date().getTime()) { iframeMsg("update", { v1: GM_info.script.version, v2: obj.version, href: obj.url }); } } }); } top.addEventListener("message", event => { if (event.data.type === "jump") { GLOBAL.index++; iframeMsg("tip", { tip: "准备答第" + (GLOBAL.index + 1) + "题" }); } else if (event.data.type === "stop") { GLOBAL.stop = event.data.val; } else if (event.data.type === "start_pay") { if (event.data.flag) { if (String(GM_getValue("token")).length === 10 || String(GM_getValue("token")).length === 11) { iframeMsg("tip", { tip: "已开启请求收费题库,已实时生效" }); GM_setValue("start_pay", event.data.flag); iframeMsg("start_pay", true); } else { iframeMsg("tip", { tip: "系统检测您的token可能输入有误,请检查" }); } } else { iframeMsg("tip", { tip: "已关闭请求收费题库,已实时生效" }); GM_setValue("start_pay", event.data.flag); iframeMsg("start_pay", false); } } else if (event.data.type === "auto_jump") { GM_setValue("auto_jump", event.data.flag); iframeMsg("tip", { tip: "已" + (event.data.flag ? "开启" : "关闭") + "自动切换,页面刷新后生效" }); } else if (event.data.type === "confim") { if (event.data.token.length === 10 || event.data.token.length === 11) { GM_setValue("token", event.data.token); iframeMsg("tip", { tip: "成功设置token,请点击开启付费题库" }); } else { iframeMsg("tip", { tip: "系统检测您的token可能输入有误,请检查" }); } } else if (event.data.type === "save_setting") { GM_setValue("gpt", event.data.gpt); GM_setValue("search_delay", event.data.search_delay); GM_setValue("tiku_adapter", event.data.tiku_adapter); } }, false); $(document).keydown(function(event) { if (event.keyCode === 38) { $("." + modelId).hide(); } else if (event.keyCode === 40) { $("." + modelId).show(); } else if (event.keyCode === 37) { $("." + modelId).hide(); GM_setValue("hide", true); } else if (event.keyCode === 39) { $("." + modelId).show(); GM_setValue("hide", false); GM_setValue("pos", "50px,50px"); } else if (event.keyCode === 83) { GLOBAL.stop = true; iframeMsg("stop", GLOBAL.stop); } else if (event.keyCode === 68) { GLOBAL.stop = false; iframeMsg("stop", GLOBAL.stop); } }); function getAnswerForKey(keys, options) { return keys.map(function(val) { return options[val.charCodeAt(0) - 65]; }); } function setIntervalFunc(flag, func, time) { const interval = setInterval(() => { if (flag()) { clearInterval(interval); func(); } }, time || 1e3); } function getAnswer(str, options, type) { if (type === 0 || type === 1) { const ans = getAnswerForKey(str.match(/[A-G]/gi) || [], options); return ans.length > 0 ? ans : [ str ]; } else { return [ str ]; } } function getQuestionType(str) { if (!str) return; str = str.trim().replaceAll(/\s+/g, ""); if (TYPE[str]) return TYPE[str]; const regex = Object.keys(TYPE).join("|"); const matcher = str.match(regex); if (matcher) return TYPE[matcher[0]]; } function rand(m, n) { return Math.ceil(Math.random() * (n - m + 1) + m - 1); } const TYPE = { "阅读理解(选择)/完型填空": 66, "听力训练": 66, multichoice: 1, singlechoice: 0, SingleChoice: 0, bijudgement: 3, Judgement: 3, "单项选择题": 0, "单项选择": 0, "单选题": 0, "单选": 0, "多选": 1, "多选题": 1, "案例分析": 1, "多项选择题": 1, "多项选择": 1, "客观题": 1, "填空题": 2, "填空": 2, "对错题": 3, "判断题": 3, "判断正误": 3, "判断": 3, "主观题": 4, "问答题": 4, "简答题": 4, "名词解释": 5, "论述题": 6, "计算题": 7, "其它": 8, "分录题": 9, "资料题": 10, "连线题": 11, "排序题": 13, "完形填空": 14, "完型填空": 14, "阅读理解": 15, "口语题": 18, "听力题": 19, "A1A2题": 1, "文件作答": 4, "视频题": 1 }; function sleep(time) { return new Promise(resolve => { setTimeout(resolve, time); }); } function iframeMsg(type, message) { try { top.document.getElementById("iframeNode").contentWindow.vueDefinedProp(type, message); } catch (e) {} } function filterImg(dom) { if (location.host === "ncexam.cug.edu.cn") { String.prototype.trim = function() { return this.replace(/^\s+|\s+$/gm, ""); }; } return $(dom).clone().find("img[src]").replaceWith(function() { return $("

").text(''); }).end().find("iframe[src]").replaceWith(function() { return $("

").text('
运行日志
[00:00:00]如果此提示不消失,说明页面出现了错误,请联系作者
任务配置
如果显示视频已观看完毕,但视频任务未完成,可以开高倍速或视频优化功能,多刷几次❗注意哦倍速不要改太高
视频倍速:
保存  |  复习模式  |  查看学习进度  |  后台挂机   激活挂机  |  模式: 遍历  |  自动阅读: 开启 打开阅读界面自动阅读  |  视频题目: 开启 自动处理视频内题目
章节测试:
自动答题 |  自动提交 |  自动保存
章节测试正确率(百分比):
保存   在答题正确率在规定之上并且允许自动提交时才会提交答案
🚀 视频优化:
智能倍速调节  |  时长补偿机制  |  行为模拟优化  |  检测规避技术  |  风险提示   ⚠️ 高倍速使用存在被检测风险,建议适度使用。开启优化功能可降低风险,但不能完全避免。
考试功能:
打开考试界面后自动显示  |  多端学习: 解除 解除多端学习监控,开启此功能后可以多端学习。
题库配置

关注微信公众号:一之哥哥,发送 "token" 领取你的token,可以提高答题并发数量。

领取到token后,填入输入框中,点击保存即可。还有undefined是没有设定,而不是有效token

题库Token:
保存
自动登录配置
⚠️ 风险提示:超星官方禁止自动登录脚本,使用时可能触发账号风控。建议不要自动登录,当心风控。
自动登录: 关闭
保存登录信息
`; top.document.body.appendChild(floatWin); // 悬浮窗样式 const style = document.createElement("style"); style.innerHTML = ` #xxt-helper-window { position: fixed; top: 60px; left: 60px; width: 720px; /* 原来900px,缩小为原来的80% */ height: 560px; /* 原来700px,缩小为原来的80% */ background: #fff; border: 2px solid #444; border-radius: 8px; box-shadow: 0 6px 20px rgba(0,0,0,0.25); z-index: 2147483647 !important; /* 使用最大可能的z-index值 */ display: flex; flex-direction: column; overflow: visible !important; /* 允许内容溢出 */ min-width: 240px; /* 原来300px,缩小为原来的80% */ min-height: 160px; /* 原来200px,缩小为原来的80% */ transform: none !important; /* 防止被父容器的transform影响 */ max-width: 720px !important; /* 限制最大宽度与原始宽度一致 */ max-height: 560px !important; /* 限制最大高度与原始高度一致 */ contain: none !important; /* 防止被CSS containment限制 */ clip: auto !important; /* 防止被裁剪 */ pointer-events: auto !important; /* 确保鼠标事件正常工作 */ visibility: visible !important; /* 确保可见性 */ opacity: 1 !important; /* 确保不透明 */ box-sizing: content-box !important; /* 确保边框不会增加元素实际大小 */ padding: 0 !important; /* 移除内边距 */ margin: 0 !important; /* 移除外边距 */ font-size: 14px !important; /* 适当缩小字体 */ } #xxt-helper-header { background: #444; color: #fff; padding: 8px 12px; cursor: move; display: flex; align-items: center; flex-shrink: 0; /* 防止头部被压缩 */ } #xxt-helper-header button { background: transparent; color: #fff; border: none; margin-left: 8px; cursor: pointer; font-size: 14px; padding: 4px 8px; border-radius: 3px; transition: background-color 0.2s; } #xxt-helper-header button:hover { background: rgba(255,255,255,0.2); } #xxt-helper-content { flex: 1; overflow: auto; /* 改为auto显示滚动条 */ padding: 15px; display: flex; flex-direction: column; z-index: 999999; /* 增加层级保证显示 */ } /* 配置标签样式 */ #config-tabs { display: flex; gap: 5px; margin-bottom: 15px; border-bottom: 2px solid #eee; flex-shrink: 0; } .config-tab { background: #f5f5f5; border: 1px solid #ddd; border-bottom: none; padding: 8px 16px; cursor: pointer; border-radius: 4px 4px 0 0; font-size: 14px; transition: all 0.2s; white-space: nowrap; } .config-tab:hover { background: #e9e9e9; } .config-tab.active { background: #fff; border-color: #007bff; color: #007bff; font-weight: bold; } /* 配置面板样式 */ .config-panel { display: none; flex: 1; overflow-y: auto; padding: 10px 0; } .config-panel.active { display: block; } /* Bootstrap面板样式优化 */ .panel { margin-bottom: 15px; border: 1px solid #ddd; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .panel-heading { background: #f5f5f5; border-bottom: 1px solid #ddd; padding: 10px 15px; font-weight: bold; border-radius: 4px 4px 0 0; } .panel-body { padding: 15px; } .panel-info { border-color: #bce8f1; } .panel-info > .panel-heading { background: #d9edf7; border-color: #bce8f1; color: #31708f; } /* 按钮样式优化 */ .btn { display: inline-block; padding: 6px 12px; margin-bottom: 0; font-size: 14px; font-weight: normal; line-height: 1.42857143; text-align: center; white-space: nowrap; vertical-align: middle; cursor: pointer; border: 1px solid transparent; border-radius: 4px; text-decoration: none; transition: all 0.2s; } .btn-default { color: #333; background-color: #fff; border-color: #ccc; } .btn-default:hover { color: #333; background-color: #e6e6e6; border-color: #adadad; } .btn-success { color: #fff; background-color: #5cb85c; border-color: #4cae4c; } .btn-success:hover { color: #fff; background-color: #449d44; border-color: #398439; } /* 输入框样式 */ input[type="text"], input[type="number"], input[type="password"] { display: inline-block; padding: 6px 12px; font-size: 14px; line-height: 1.42857143; color: #555; background-color: #fff; background-image: none; border: 1px solid #ccc; border-radius: 4px; box-shadow: inset 0 1px 1px rgba(0,0,0,.075); transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; } input[type="text"]:focus, input[type="number"]:focus, input[type="password"]:focus { border-color: #66afe9; outline: 0; box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6); } /* 响应式设计 */ @media (max-width: 1200px) { #xxt-helper-window { width: 640px; /* 原来800px,缩小为原来的80% */ height: 480px; /* 原来600px,缩小为原来的80% */ } } @media (max-width: 1000px) { #xxt-helper-window { width: 90vw; /* 改用视窗单位 */ height: 90vh; left: 5vw; top: 5vh; } .config-tab { padding: 6px 12px; font-size: 12px; } } /* 确保拖动时不被截断,但保持原始大小 */ #xxt-helper-window.dragging { max-width: 720px !important; max-height: 560px !important; } `; // 将样式添加到顶级窗口 try { const topWindow = window.top || window; topWindow.document.head.appendChild(style); } catch (e) { // 如果出现跨域问题,退回到当前document document.head.appendChild(style); } // 标签切换功能 function initTabSwitching() { const tabs = top.document.querySelectorAll('.config-tab'); const panels = top.document.querySelectorAll('.config-panel'); tabs.forEach(tab => { tab.addEventListener('click', () => { // 移除所有活动状态 tabs.forEach(t => t.classList.remove('active')); panels.forEach(p => p.classList.remove('active')); // 添加当前活动状态 tab.classList.add('active'); const targetPanel = top.document.getElementById(tab.dataset.tab); if (targetPanel) { targetPanel.classList.add('active'); } }); }); } // 初始化标签切换 setTimeout(initTabSwitching, 100); // 拖动逻辑 function setupDragging() { const header = top.document.getElementById("xxt-helper-header"); let isDown = false, offsetX, offsetY; header.addEventListener("mousedown", e => { isDown = true; offsetX = e.clientX - floatWin.offsetLeft; offsetY = e.clientY - floatWin.offsetTop; floatWin.classList.add('dragging'); // 添加拖动类 e.preventDefault(); // 防止文本选择 }); top.document.addEventListener("mousemove", e => { if (!isDown) return; const newLeft = e.clientX - offsetX; const newTop = e.clientY - offsetY; // 允许拖出屏幕边界,实现真正的全局拖动 floatWin.style.left = newLeft + "px"; floatWin.style.top = newTop + "px"; }); top.document.addEventListener("mouseup", () => { isDown = false; floatWin.classList.remove('dragging'); // 移除拖动类 }); // 防止拖拽时选中文本 header.addEventListener("selectstart", e => { e.preventDefault(); }); } // 初始化拖拽功能 setTimeout(setupDragging, 100); // 按钮逻辑 - 立即设置,不等待其他条件 // 悬浮窗初始化函数 function initFloatingWindow() { const minBtn = top.document.getElementById("xxt-min"); const maxBtn = top.document.getElementById("xxt-max"); const closeBtn = top.document.getElementById("xxt-close"); if (minBtn) { minBtn.onclick = () => { if (!isMinimized) { // 最小化:只显示标题栏 top.document.getElementById("xxt-helper-content").style.display = "none"; top.document.getElementById("xxt-helper-window").style.height = "auto"; top.document.getElementById("xxt-helper-window").style.minHeight = "auto"; isMinimized = true; } else { // 恢复:显示完整内容 top.document.getElementById("xxt-helper-content").style.display = "flex"; top.document.getElementById("xxt-helper-window").style.height = "560px"; // 缩小为原来的80% top.document.getElementById("xxt-helper-window").style.minHeight = "160px"; // 缩小为原来的80% isMinimized = false; } }; } if (maxBtn) { maxBtn.onclick = () => { if (!isMax) { // 全屏模式 - 确保不会被截断,但不要太大 floatWin.style.width = "720px"; // 缩小为原来的80% floatWin.style.height = "560px"; // 缩小为原来的80% // 居中显示 floatWin.style.top = "50%"; floatWin.style.left = "50%"; floatWin.style.transform = "translate(-50%, -50%)"; } else { // 恢复默认大小和位置 floatWin.style.width = "720px"; // 缩小为原来的80% floatWin.style.height = "560px"; // 缩小为原来的80% floatWin.style.top = "60px"; floatWin.style.left = "60px"; floatWin.style.transform = "none"; } isMax = !isMax; }; } if (closeBtn) { closeBtn.onclick = () => { floatWin.style.display = "none"; // 移除弹窗提示,直接关闭 }; } } // 设置按钮事件处理 setTimeout(initFloatingWindow, 200); // 确保悬浮窗始终可见的检查函数 function ensureFloatingWindowVisible() { let floatWin = top.document.getElementById("xxt-helper-window"); // 如果悬浮窗不存在,重新创建 if (!floatWin) { logs.addLog("悬浮窗不存在,正在重新创建...", 'orange'); createFloatingWindow(); return; } // 如果悬浮窗被隐藏,恢复显示 if (floatWin.style.display === "none") { floatWin.style.display = "flex"; logs.addLog("悬浮窗已恢复显示", 'green'); } } // 重新创建悬浮窗的函数 function createFloatingWindow() { try { // 检查是否已存在悬浮窗,避免重复创建 let existingFloatWin = top.document.getElementById("xxt-helper-window"); if (existingFloatWin) { existingFloatWin.remove(); } // 创建悬浮窗 - 确保创建在顶级文档中 const floatWin = top.document.createElement("div"); floatWin.id = "xxt-helper-window"; floatWin.style.cssText = ` position: fixed !important; top: 60px !important; left: 60px !important; width: 720px !important; height: 560px !important; background: #fff !important; border: 2px solid #444 !important; border-radius: 8px !important; box-shadow: 0 6px 20px rgba(0,0,0,0.25) !important; z-index: 2147483647 !important; display: flex !important; flex-direction: column !important; overflow: visible !important; min-width: 240px !important; min-height: 160px !important; transform: none !important; max-width: 720px !important; max-height: 560px !important; contain: none !important; clip: auto !important; `; // 添加悬浮窗内容 floatWin.innerHTML = `
📚 学习通助手

学习通助手

悬浮窗已重新创建

如果看不到完整界面,请刷新页面

`; // 添加到页面 top.document.body.appendChild(floatWin); // 重新初始化悬浮窗功能 setTimeout(initFloatingWindow, 100); logs.addLog("悬浮窗重新创建成功", 'green'); } catch (e) { logs.addLog("创建悬浮窗失败: " + e.message, 'red'); } } // 定期检查悬浮窗状态 setInterval(ensureFloatingWindowVisible, 3000); // 立即检查并创建悬浮窗 setTimeout(() => { ensureFloatingWindowVisible(); }, 1000); // 添加一个简单的测试悬浮窗,确保基本功能 setTimeout(() => { // 检查是否有悬浮窗 const testFloatWin = top.document.getElementById("xxt-helper-window"); if (!testFloatWin) { logs.addLog("检测到悬浮窗不存在,正在创建测试悬浮窗...", 'orange'); // 创建一个简单的测试悬浮窗 const testWin = top.document.createElement("div"); testWin.id = "test-float-window"; testWin.style.cssText = ` position: fixed !important; top: 20px !important; right: 20px !important; width: 300px !important; height: 200px !important; background: #fff !important; border: 2px solid #ff0000 !important; border-radius: 8px !important; box-shadow: 0 4px 20px rgba(0,0,0,0.3) !important; z-index: 2147483647 !important; padding: 15px !important; font-family: Arial, sans-serif !important; `; testWin.innerHTML = `

⚠️ 悬浮窗测试

原始悬浮窗可能有问题

请刷新页面重新加载脚本

`; top.document.body.appendChild(testWin); // 5秒后自动关闭测试窗 setTimeout(() => { if (testWin.parentElement) { testWin.remove(); } }, 5000); } }, 2000); // ==================== 阅读助手页面操作类 ==================== // 页面操作类 class PageOperator { constructor() { this.init(); } init() { this.bindEvents(); this.setupGlobalKeyListener(); // 新增:设置全局键盘监听 this.detectPageType(); this.detectTaskDuration(); // 新增:检测任务时长 readingLog(`脚本初始化,版本:${GM_info.script.version}`); } // 新增:设置全局键盘监听器作为备用 setupGlobalKeyListener() { // 移除旧的全局监听器 if (this.globalKeyHandler) { document.removeEventListener('keydown', this.globalKeyHandler); } // 创建全局键盘处理器 this.globalKeyHandler = (e) => { // 只处理S键,确保设置面板始终可用 if (e.key.toLowerCase() === 's' && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') { e.preventDefault(); e.stopPropagation(); readingLog('全局S键被按下,显示设置面板'); this.showSettings(); } }; // 绑定全局监听器 document.addEventListener('keydown', this.globalKeyHandler, true); readingLog('全局键盘监听器已设置'); } // 新增:检测任务时长 detectTaskDuration() { try { // 尝试从页面中检测任务时长 const durationSelectors = [ '.task-duration', '.duration', '.time-limit', '[class*="duration"]', '[class*="time"]', '.task-info', '.course-info' ]; let detectedDuration = 0; for (const selector of durationSelectors) { const elements = document.querySelectorAll(selector); for (const el of elements) { const text = el.textContent; // 匹配各种时长格式:30分钟、30min、30分钟阅读等 const durationMatch = text.match(/(\d+)\s*(分钟|min|分钟阅读|分钟学习)/i); if (durationMatch) { detectedDuration = parseInt(durationMatch[1]); break; } } if (detectedDuration > 0) break; } if (detectedDuration > 0) { READING_CONFIG.taskDuration = detectedDuration; GM_setValue('taskDuration', detectedDuration); readingNotify(`检测到任务时长:${detectedDuration}分钟`); this.calculateOptimalSpeed(); } else { readingLog('未检测到任务时长,使用用户设置的时长'); } } catch (error) { readingLog(`检测任务时长失败: ${error.message}`, 'error'); } } // 新增:计算最优阅读速度 calculateOptimalSpeed() { if (!READING_CONFIG.taskDuration || !READING_CONFIG.autoCalculateSpeed) return; try { // 计算内容长度 this.collectContentElements(); READING_STATE.contentLength = READING_STATE.contentElements.reduce((total, el) => { return total + (el.textContent || '').length; }, 0); if (READING_STATE.contentLength === 0) { readingLog('无法计算内容长度,跳过速度计算'); return; } // 根据时长和内容长度计算速度 const targetDurationSeconds = READING_CONFIG.taskDuration * 60; const estimatedReadingTime = READING_STATE.contentLength / 500; // 假设每分钟阅读500字 const speedRatio = estimatedReadingTime / targetDurationSeconds; let optimalSpeed = READING_CONFIG.scrollSpeed * speedRatio; optimalSpeed = Math.max(READING_CONFIG.minSpeed, Math.min(READING_CONFIG.maxSpeed, optimalSpeed)); READING_CONFIG.scrollSpeed = parseFloat(optimalSpeed.toFixed(1)); GM_setValue('scrollSpeed', READING_CONFIG.scrollSpeed.toString()); readingNotify(`已根据任务时长(${READING_CONFIG.taskDuration}分钟)自动调整阅读速度为:${READING_CONFIG.scrollSpeed}秒/${READING_CONFIG.scrollMode === 'paragraph' ? '段落' : '页'}`); readingLog(`内容长度:${READING_STATE.contentLength}字符,预估阅读时间:${estimatedReadingTime.toFixed(1)}分钟,调整后速度:${READING_CONFIG.scrollSpeed}秒/单位`); } catch (error) { readingLog(`计算最优速度失败: ${error.message}`, 'error'); } } resetState() { READING_STATE.isRunning = false; READING_STATE.isPaused = false; READING_STATE.contentElements = []; READING_STATE.currentElementIndex = 0; readingLog('状态已重置'); } bindEvents() { // 移除旧的事件监听器 if (this.boundKeyDownHandler) { document.removeEventListener('keydown', this.boundKeyDownHandler); } // 绑定新的事件监听器 this.boundKeyDownHandler = this.handleKeyDown.bind(this); document.addEventListener('keydown', this.boundKeyDownHandler); } handleKeyDown(e) { // 防止在输入框中触发快捷键 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; // 确保S键在所有页面都能工作 if (e.key.toLowerCase() === 's') { e.preventDefault(); e.stopPropagation(); readingLog('S键被按下,显示设置面板'); this.showSettings(); return; } // 其他快捷键只在特定页面工作 switch(e.key.toLowerCase()) { case 'k': e.preventDefault(); if (this.isReadingPage()) { this.startAutoRead(); } break; case 'z': e.preventDefault(); if (this.isReadingPage()) { this.pauseAutoRead(); } break; case 'j': e.preventDefault(); // 新增:手动测试自动跳转功能 if (this.isCourseCatalogPage()) { readingLog('手动触发自动跳转测试'); this.handleCourseCatalogPage(); } else { readingNotify('当前页面不是课程目录页面', 'warning'); } break; } } detectPageType() { const oldTips = document.querySelector('#auto-read-tips'); if (oldTips) oldTips.remove(); if (this.isReadingPage()) { readingLog('检测到阅读页面'); this.initReading(); } else if (this.isCoursePage()) { readingLog('检测到课程主页'); } else if (location.href.includes('/mooc-ans/course/')) { readingLog('检测到课程目录/任务列表页面 (新)'); this.handleCourseCatalogPage(); } else if (this.isTaskPage()) { readingLog('检测到任务页面 (旧)'); } else if (this.isCourseCatalogPage()) { readingLog('检测到课程目录页面'); this.handleCourseCatalogPage(); } } isReadingPage() { return location.href.includes('/ztnodedetailcontroller/visitnodedetail'); } isCoursePage() { return location.href.includes('/mooc-ans/mycourse/studentstudy'); } isTaskPage() { return location.href.includes('pageHeader=0'); } isCourseCatalogPage() { // 检测课程目录页面的URL模式 const pathname = location.pathname; const href = location.href; // 更宽松的URL匹配规则 const urlPatterns = [ /\/mooc2-ans\/course\/\d+\.html$/, /\/mooc2-ans\/zt\/\d+\.html$/, /\/mooc-ans\/course\/\d+\.html$/, /\/mooc-ans\/zt\/\d+\.html$/, /\/course\/\d+\.html$/, /\/zt\/\d+\.html$/, /\/mooc-ans\/course\/\d+$/, /\/mooc-ans\/zt\/\d+$/, /\/course\/\d+$/, /\/zt\/\d+$/ ]; // 检查URL模式 const urlMatch = urlPatterns.some(pattern => pattern.test(pathname)); // 检查页面内容特征 const hasCourseSection = document.querySelector('.course_section') !== null; const hasChapterText = document.querySelector('.chapterText') !== null; const hasCatalogItems = document.querySelector('.catalog-item') !== null; readingLog(`页面检测: URL=${pathname}, URL匹配=${urlMatch}, 有课程章节=${hasCourseSection}, 有章节文本=${hasChapterText}, 有目录项=${hasCatalogItems}`); return urlMatch && (hasCourseSection || hasChapterText || hasCatalogItems); } // 处理课程目录页面 - 自动进入第一章节 handleCourseCatalogPage() { if (!READING_CONFIG.autoEnterChapter) { readingLog('自动进入章节功能已禁用'); return; } try { readingLog('准备自动进入第一章节'); readingNotify('检测到课程目录页面,即将自动进入第一章节...'); setTimeout(() => { // 尝试多种选择器来找到第一章节 const selectors = [ '.course_section:first-child .chapterText', '.course_section .chapterText:first-child', '.course_section .chapterText', '.catalog-item:first-child', '.catalog-item', '.chapter-item:first-child', '.chapter-item', 'a[href*="/ztnodedetailcontroller/visitnodedetail"]:first-child', 'a[href*="/ztnodedetailcontroller/visitnodedetail"]' ]; let firstChapter = null; for (const selector of selectors) { firstChapter = document.querySelector(selector); if (firstChapter) { readingLog(`使用选择器找到章节: ${selector}`); break; } } if (firstChapter) { readingLog('找到第一章节,正在点击进入'); // 高亮显示即将点击的元素 firstChapter.style.outline = '3px solid red'; firstChapter.style.transition = 'outline 0.3s ease'; // 滚动到元素位置 firstChapter.scrollIntoView({ behavior: 'smooth', block: 'center' }); // 点击元素 firstChapter.click(); readingNotify('已自动进入第一章节'); // 移除高亮 setTimeout(() => { firstChapter.style.outline = ''; }, 2000); } else { readingLog('未找到第一章节按钮', 'warning'); readingNotify('未找到第一章节,请手动点击', 'warning'); // 输出页面结构信息用于调试 const courseSections = document.querySelectorAll('.course_section'); const chapterTexts = document.querySelectorAll('.chapterText'); const catalogItems = document.querySelectorAll('.catalog-item'); readingLog(`调试信息: course_section数量=${courseSections.length}, chapterText数量=${chapterTexts.length}, catalog-item数量=${catalogItems.length}`); if (courseSections.length > 0) { readingLog(`第一个course_section内容: ${courseSections[0].innerHTML.substring(0, 200)}...`); } } }, READING_CONFIG.chapterEnterDelay); } catch (error) { readingLog(`自动进入章节失败: ${error.message}`, 'error'); readingNotify('自动进入章节失败', 'error'); } } initReading() { this.showUsageTips(); this.detectChapterInfo(); this.collectContentElements(); if (READING_CONFIG.autoStart) { setTimeout(() => { if (this.isReadingPage()) { this.startAutoRead(); } }, 2000); } } startAutoRead() { if (READING_STATE.isRunning && !READING_STATE.isPaused) return; READING_STATE.isRunning = true; READING_STATE.isPaused = false; if (READING_CONFIG.scrollMode === 'paragraph') { this.startParagraphScroll(); } else { this.startPageScroll(); } this.showStatus(); readingNotify(`开始阅读 (${READING_CONFIG.scrollSpeed}秒/${READING_CONFIG.scrollMode === 'paragraph' ? '段落' : '页'})`); } pauseAutoRead() { if (!READING_STATE.isRunning) return; READING_STATE.isPaused = true; clearTimeout(this.scrollTimer); this.showStatus(); readingNotify('阅读已暂停'); } showStatus() { const status = READING_STATE.isPaused ? '已暂停' : '阅读中'; const progress = READING_STATE.totalChapters > 0 ? `第 ${READING_STATE.currentChapter}/${READING_STATE.totalChapters} 章` : '章节信息未知'; this.showModal(`

超星阅读助手

状态: ${status}

${progress}

模式: ${READING_CONFIG.scrollMode === 'paragraph' ? '段落阅读' : '页面阅读'}

速度: ${READING_CONFIG.scrollSpeed.toFixed(1)}秒/${READING_CONFIG.scrollMode === 'paragraph' ? '段落' : '页'}

按 Z 键暂停 / 按 K 键继续

`); setTimeout(() => this.hideModal(), 3000); } startParagraphScroll() { this.collectContentElements(); if (READING_STATE.contentElements.length === 0) { readingNotify('未检测到段落,临时切换整页模式...', 'warning'); READING_CONFIG.scrollMode = 'page'; this.startPageScroll(); // 启动内容监听器 const observer = new MutationObserver(() => { this.collectContentElements(); if (READING_STATE.contentElements.length > 0) { observer.disconnect(); readingNotify('检测到段落内容,恢复逐段模式', 'success'); READING_CONFIG.scrollMode = 'paragraph'; if (!READING_STATE.isPaused) { this.startParagraphScroll(); } } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true }); return; } this.scrollToNextElement(); } collectContentElements() { const selectors = [ 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img', 'video', 'iframe', '.content', '.text-block', '.article-content' ]; READING_STATE.contentElements = []; selectors.forEach(selector => { const elements = document.querySelectorAll(selector); elements.forEach(el => { if (el.offsetHeight > 20 && el.offsetWidth > 20) { READING_STATE.contentElements.push(el); } }); }); READING_STATE.contentElements.sort((a, b) => { return a.getBoundingClientRect().top - b.getBoundingClientRect().top; }); readingLog(`找到 ${READING_STATE.contentElements.length} 个内容元素`); } scrollToNextElement() { if (READING_STATE.isPaused) return; if (READING_STATE.currentElementIndex >= READING_STATE.contentElements.length) { this.onChapterComplete(); return; } const element = READING_STATE.contentElements[READING_STATE.currentElementIndex]; if (READING_CONFIG.debugMode) { element.style.outline = '2px solid red'; setTimeout(() => element.style.outline = '', 1000); } element.scrollIntoView({ behavior: 'smooth', block: 'center' }); const baseTime = parseFloat(READING_CONFIG.scrollSpeed) * 1000; let waitTime = element.tagName === 'IMG' || element.tagName === 'VIDEO' ? baseTime * 1.5 : baseTime; READING_STATE.currentElementIndex++; this.scrollTimer = setTimeout(() => this.scrollToNextElement(), waitTime); } startPageScroll() { const scrollSpeed = parseFloat(READING_CONFIG.scrollSpeed); const totalHeight = document.documentElement.scrollHeight - window.innerHeight; const scrollStep = totalHeight / (scrollSpeed * 10); let currentTop = 0; clearInterval(this.scrollTimer); this.scrollTimer = setInterval(() => { if (READING_STATE.isPaused) return; currentTop += scrollStep; if (currentTop >= totalHeight) { clearInterval(this.scrollTimer); this.onChapterComplete(); } else { window.scrollTo({ top: currentTop, behavior: 'smooth' }); } }, 100); } onChapterComplete() { clearTimeout(this.scrollTimer); READING_STATE.currentElementIndex = 0; READING_STATE.contentElements = []; this.findNextButton() .then(nextButton => { if (nextButton) { readingNotify('正在加载下一章...'); nextButton.style.outline = '3px solid green'; setTimeout(() => nextButton.style.outline = '', 2000); nextButton.click(); READING_STATE.currentChapter++; } else { if (READING_CONFIG.restartAfterFinish) { readingNotify('已到达最后一章,即将从头开始', 'warning'); setTimeout(() => this.goToFirstChapter(), 3000); } else { READING_STATE.isRunning = false; readingNotify('全部阅读完成!', 'success'); } } }) .catch(error => { readingNotify(`章节切换错误: ${error.message}`, 'error'); readingLog(error.stack, 'error'); }); } findNextButton() { return new Promise(resolve => { const selectors = [ READING_CONFIG.nextButtonSelector, '.nodeItem.r i', '.next-page-btn', 'a:contains("下一章")', 'button:contains("下一章")', 'a[title="下一章"]', '.reader__control--next', '.uxp-pager-next', 'button[aria-label*="下一章"]', '.next-btn:visible' ]; for (const selector of selectors) { try { let element = document.querySelector(selector); if (!element && selector.includes(':contains(')) { const text = selector.match(/:contains\("(.*)"\)/)[1]; const allElements = document.querySelectorAll('a, button'); element = Array.from(allElements).find(el => el.textContent.includes(text)); } if (element) { readingLog(`找到下一章按钮: ${selector}`); return resolve(element); } } catch (error) { // 继续尝试下一个选择器 } } readingLog('未找到下一章按钮,尝试通用选择器', 'warning'); const genericElements = document.querySelectorAll('*'); for (const el of genericElements) { if (el.textContent.includes('下一章') && el.offsetWidth > 0 && el.offsetHeight > 0) { readingLog('找到下一章按钮(通用选择器)'); return resolve(el); } } readingLog('未找到下一章按钮', 'warning'); resolve(null); }); } goToFirstChapter() { try { const firstChapter = document.querySelector('.course_section .chapterText, .catalog-item:first-child'); if (firstChapter) { firstChapter.click(); setTimeout(() => { READING_STATE.currentChapter = 1; if (READING_CONFIG.scrollMode === 'paragraph') { this.collectContentElements(); } if (!READING_STATE.isPaused) { this.scrollToNextElement(); } }, 3000); } else { readingNotify('未找到目录,无法重新开始', 'error'); READING_STATE.isRunning = false; } } catch (error) { readingNotify(`跳转错误: ${error.message}`, 'error'); READING_STATE.isRunning = false; } } // 设置菜单 showSettings() { const html = `

超星阅读助手设置

生活不易,猪猪叹气 —— 赏口饲料,让我少气!🐷✨

输入任务要求的时长,脚本会自动计算最优阅读速度
根据任务时长自动调整阅读速度
支持 0.1 - 30 秒(如:0.5 秒/页,1.2 秒/段落)
在课程目录页面自动点击第一章节
单位: 毫秒,延迟时间越长越稳定
`; this.showModal(html); document.getElementById('save-settings').addEventListener('click', () => { READING_CONFIG.taskDuration = parseInt(document.getElementById('task-duration').value); READING_CONFIG.autoCalculateSpeed = document.getElementById('auto-calculate-speed').checked; READING_CONFIG.scrollSpeed = parseFloat(document.getElementById('scroll-speed').value).toFixed(1); READING_CONFIG.scrollMode = document.getElementById('scroll-mode').value; READING_CONFIG.autoStart = document.getElementById('auto-start').checked; READING_CONFIG.restartAfterFinish = document.getElementById('restart-after-finish').checked; READING_CONFIG.showTips = document.getElementById('show-tips').checked; READING_CONFIG.debugMode = document.getElementById('debug-mode').checked; READING_CONFIG.autoEnterChapter = document.getElementById('auto-enter-chapter').checked; READING_CONFIG.chapterEnterDelay = parseInt(document.getElementById('chapter-enter-delay').value); GM_setValue('taskDuration', READING_CONFIG.taskDuration); GM_setValue('autoCalculateSpeed', READING_CONFIG.autoCalculateSpeed); GM_setValue('scrollSpeed', READING_CONFIG.scrollSpeed.toString()); GM_setValue('scrollMode', READING_CONFIG.scrollMode); GM_setValue('autoStart', READING_CONFIG.autoStart); GM_setValue('restartAfterFinish', READING_CONFIG.restartAfterFinish); GM_setValue('showTips', READING_CONFIG.showTips); GM_setValue('debugMode', READING_CONFIG.debugMode); GM_setValue('autoEnterChapter', READING_CONFIG.autoEnterChapter); GM_setValue('chapterEnterDelay', READING_CONFIG.chapterEnterDelay); // 如果启用了自动计算速度,重新计算 if (READING_CONFIG.autoCalculateSpeed && READING_CONFIG.taskDuration > 0) { this.calculateOptimalSpeed(); } readingNotify('设置已保存'); this.hideModal(); }); document.getElementById('close-settings').addEventListener('click', () => { this.hideModal(); }); // 添加测试按钮事件监听器 document.getElementById('test-page-detection').addEventListener('click', () => { const isCatalog = this.isCourseCatalogPage(); const isReading = this.isReadingPage(); const isCourse = this.isCoursePage(); const isTask = this.isTaskPage(); const result = `

页面检测结果:

当前URL: ${location.href}

当前路径: ${location.pathname}

是课程目录页: ${isCatalog ? '是' : '否'}

是阅读页面: ${isReading ? '是' : '否'}

是课程主页: ${isCourse ? '是' : '否'}

是任务页面: ${isTask ? '是' : '否'}

自动进入章节: ${READING_CONFIG.autoEnterChapter ? '已开启' : '已关闭'}

`; this.showModal(result); }); document.getElementById('test-auto-jump').addEventListener('click', () => { if (this.isCourseCatalogPage()) { readingLog('手动触发自动跳转测试'); this.handleCourseCatalogPage(); } else { readingNotify('当前页面不是课程目录页面', 'warning'); } }); document.getElementById('test-s-key').addEventListener('click', () => { readingLog('手动测试S键功能'); readingNotify('正在测试S键功能...'); // 模拟S键按下 const event = new KeyboardEvent('keydown', { key: 's', code: 'KeyS', keyCode: 83, which: 83, bubbles: true, cancelable: true }); document.dispatchEvent(event); setTimeout(() => { readingNotify('S键测试完成,如果设置面板没有出现,请检查控制台日志'); }, 100); }); } // 弹窗相关方法 createModal() { if (document.getElementById('auto-read-modal')) return; const modal = document.createElement('div'); modal.id = 'auto-read-modal'; modal.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.2); z-index: 9999; min-width: 300px; max-width: 80%; display: none; font-family: Arial, sans-serif; `; document.body.appendChild(modal); } showModal(content) { this.createModal(); const modal = document.getElementById('auto-read-modal'); modal.innerHTML = content; modal.style.display = 'block'; } hideModal() { const modal = document.getElementById('auto-read-modal'); if (modal) modal.style.display = 'none'; } // 显示操作提示 showUsageTips() { if (!READING_CONFIG.showTips) return; if (!location.href.includes("/ztnodedetailcontroller/visitnodedetail")) return; const oldTips = document.querySelector('#auto-read-tips'); if (oldTips) oldTips.remove(); const tips = `

超星阅读助手快捷键:

• K: 开始/继续阅读

• Z: 暂停阅读

• S: 显示设置

• J: 测试自动跳转 (目录页)

• 任务时长: ${READING_CONFIG.taskDuration > 0 ? READING_CONFIG.taskDuration + '分钟' : '未设置'}

• 自动进入章节: ${READING_CONFIG.autoEnterChapter ? '已开启' : '已关闭'}

`; document.body.insertAdjacentHTML('beforeend', tips); } // 添加快速启动按钮 addQuickStartButton() { try { const courseTitle = document.querySelector('.course-title') || document.querySelector('.course-name') || document.querySelector('h1'); if (courseTitle) { const startButton = document.createElement('button'); startButton.textContent = '🚀 开始自动阅读'; startButton.style.cssText = ` background-color: #4CAF50; color: white; border: none; padding: 8px 16px; margin-left: 10px; border-radius: 4px; cursor: pointer; font-size: 14px; `; startButton.addEventListener('click', () => { const firstChapter = document.querySelector('.course_section .chapterText, .catalog-item:first-child'); if (firstChapter) { firstChapter.click(); setTimeout(() => { this.detectChapterInfo(); this.startAutoRead(); }, 3000); } else { readingNotify('未找到章节列表', 'error'); } }); courseTitle.parentNode.insertBefore(startButton, courseTitle.nextSibling); } } catch (error) { readingLog(`添加快速启动按钮失败: ${error.message}`, 'error'); } } // 检测章节信息 detectChapterInfo() { try { const chapterTitle = document.querySelector('.node-title, .chapter-title, h1')?.textContent || '未知章节'; const chapterElements = document.querySelectorAll('.course_section .chapterText, .catalog-item'); const currentChapterElement = document.querySelector('.course_section .chapterText.active, .catalog-item.active'); if (chapterElements.length > 0) { READING_STATE.totalChapters = chapterElements.length; if (currentChapterElement) { const chapterArray = Array.from(chapterElements); READING_STATE.currentChapter = chapterArray.indexOf(currentChapterElement) + 1; readingLog(`当前章节: ${READING_STATE.currentChapter}/${READING_STATE.totalChapters} - ${chapterTitle}`); } else { readingLog(`总章节数: ${READING_STATE.totalChapters} - 无法确定当前章节`); } } else { readingLog('无法获取章节信息', 'warning'); } } catch (error) { readingLog(`检测章节信息失败: ${error.message}`, 'error'); } } } // 初始化页面操作类 let pageOperator = null; try { console.log('开始初始化PageOperator...'); pageOperator = new PageOperator(); console.log('PageOperator初始化成功'); } catch (error) { console.error('初始化阅读助手失败:', error); console.error(`PageOperator初始化失败: ${error.message}`); console.error(`错误堆栈: ${error.stack}`); } // 强制检测当前页面类型(用于调试) setTimeout(() => { try { console.log('=== 开始强制页面检测 ==='); console.log(`当前URL: ${location.href}`); console.log(`当前路径: ${location.pathname}`); console.log(`PageOperator是否存在: ${pageOperator ? '是' : '否'}`); if (pageOperator) { console.log(`是课程目录页: ${pageOperator.isCourseCatalogPage()}`); console.log(`是阅读页面: ${pageOperator.isReadingPage()}`); console.log(`是课程主页: ${pageOperator.isCoursePage()}`); console.log(`是任务页面: ${pageOperator.isTaskPage()}`); console.log(`自动进入章节: ${READING_CONFIG.autoEnterChapter ? '已开启' : '已关闭'}`); // 如果是课程目录页面,强制触发自动跳转 if (pageOperator.isCourseCatalogPage() && READING_CONFIG.autoEnterChapter) { console.log('检测到课程目录页面,强制触发自动跳转'); pageOperator.handleCourseCatalogPage(); } } else { console.log('PageOperator未初始化,尝试重新初始化...'); try { pageOperator = new PageOperator(); console.log('PageOperator重新初始化成功'); } catch (error) { console.error(`PageOperator重新初始化失败: ${error.message}`); } } } catch (error) { console.error('强制页面检测失败:', error); } }, 1000); var logs = { "logArry": [], "addLog": function (str, color = "black") { if (this.logArry.length >= 50) { this.logArry.splice(0, 1); } var nowTime = new Date(); var nowHour = (Array(2).join(0) + nowTime.getHours()).slice(-2); var nowMin = (Array(2).join(0) + nowTime.getMinutes()).slice(-2); var nowSec = (Array(2).join(0) + nowTime.getSeconds()).slice(-2); this.logArry.push("[" + nowHour + ":" + nowMin + ":" + nowSec + "] " + str + ""); let logStr = ""; for (let logI = 0, logLen = this.logArry.length; logI < logLen; logI++) { logStr += this.logArry[logI] + "
"; } try { top.document.getElementById('log').innerHTML = logStr; var logElement = top.document.getElementById('log'); logElement.scrollTop = logElement.scrollHeight; } catch (e) { console.error("日志更新失败:", e); } }, "getLogs": function () { return this.logArry.map(log => { // 移除HTML标签和时间戳,只返回纯文本内容 return log.replace(/<[^>]*>/g, '').replace(/\[\d{2}:\d{2}:\d{2}\]\s*/, ''); }); } }, htmlHook = setInterval(function () { if (top.document.getElementById('unrivalRate') && top.document.getElementById('updateRateButton') && top.document .getElementById('reviewModeButton') && top.document.getElementById('autoDoWorkButton') && top.document .getElementById('autoSubmitButton') && top.document.getElementById('autoSaveButton')) { if (!backGround) { top.document.getElementById('fuckMeModeButton').style.display = "none"; } allowBackground = Math.round(new Date() / 1000) - parseInt(GM_getValue( 'unrivalBackgroundVideoEnable', '6')) < 15; if (allowBackground) { top.document.getElementById('fuckMeModeButton').setAttribute('href', 'unrivalxxtbackground/'); } clearInterval(htmlHook); if (cVersion < 86) { logs.addLog( '\u60a8\u7684\u6d4f\u89c8\u5668\u5185\u6838\u8fc7\u8001\uff0c\u8bf7\u66f4\u65b0\u7248\u672c\u6216\u4f7f\u7528\u4e3b\u6d41\u6d4f\u89c8\u5668\uff0c\u63a8\u8350\u003c\u0061\u0020\u0068\u0072\u0065\u0066\u003d\u0022\u0068\u0074\u0074\u0070\u0073\u003a\u002f\u002f\u0077\u0077\u0077\u002e\u006d\u0069\u0063\u0072\u006f\u0073\u006f\u0066\u0074\u002e\u0063\u006f\u006d\u002f\u007a\u0068\u002d\u0063\u006e\u002f\u0065\u0064\u0067\u0065\u0022\u0020\u0074\u0061\u0072\u0067\u0065\u0074\u003d\u0022\u0076\u0069\u0065\u0077\u005f\u0077\u0069\u006e\u0064\u006f\u0077\u0022\u003e\u0065\u0064\u0067\u0065\u6d4f\u89c8\u5668\u003c\u002f\u0061\u003e', 'red'); stop = true; return; } if (isMobile) { logs.addLog('手机浏览器不保证能正常运行此脚本', 'orange'); } _d.addEventListener('visibilitychange', function () { let isH = _d.hidden; if (!isH) { logs.addLog('挂机功能不稳定,不建议长时间最小化窗口', 'orange'); } }); top.document.getElementById('unrivalRate').value = rate; // 🚀 新增:初始化优化功能按钮状态 updateOptimizationButtonState('smartSpeedControl', smartSpeedControl); updateOptimizationButtonState('durationCompensation', durationCompensation); updateOptimizationButtonState('behaviorSimulation', behaviorSimulation); updateOptimizationButtonState('detectionEvasion', detectionEvasion); updateOptimizationButtonState('riskWarning', riskWarning); // 🚀 新增:初始化风险提示 if (riskWarning && rate > 1.5) { setTimeout(() => { if (rate > 3) { logs.addLog('🚨 检测到高风险倍速设置!强烈建议开启所有优化功能', 'red'); } else if (rate > 2) { logs.addLog('⚠️ 检测到中等风险倍速,建议开启优化功能', 'orange'); } else { logs.addLog('💡 建议开启优化功能以提高安全性', 'blue'); } }, 2000); } top.document.getElementById('updateToken').onclick = function () { var token = top.document.getElementById('token').value; logs.addLog('题库token已更新为' +token, 'green'); GM_setValue('tikutoken', token); } top.document.getElementById('accuracy').value=accuracy; top.document.getElementById('updateaccuracy').onclick = function () { var uaccuracy = top.document.getElementById('accuracy').value; if (parseFloat(uaccuracy) == parseInt(uaccuracy)) { uaccuracy = parseInt(uaccuracy); } else { uaccuracy = parseFloat(uaccuracy); } GM_setValue('accuracy', uaccuracy); accuracy = uaccuracy; if (uaccuracy >= 0 && uaccuracy<=100) { logs.addLog('章节测试正确率已更新为'+uaccuracy+'%,将在3秒内生效', 'green'); } else { logs.addLog('奇怪正确率', 'orange'); } } top.document.getElementById('updateRateButton').onclick = function () { let urate = top.document.getElementById('unrivalRate').value; if (parseFloat(urate) == parseInt(urate)) { urate = parseInt(urate); } else { urate = parseFloat(urate); } GM_setValue('unrivalrate', urate); rate = urate; if (urate > 0) { logs.addLog('视频倍速已更新为' + urate + '倍,将在3秒内生效', 'green'); // 🚀 新增:风险提示 if (riskWarning) { if (urate > 3) { logs.addLog('⚠️ 高风险倍速!强烈建议开启所有优化功能', 'red'); } else if (urate > 2) { logs.addLog('⚠️ 中等风险倍速,建议开启优化功能', 'orange'); } else if (urate > 1.5) { logs.addLog('💡 建议开启优化功能以提高安全性', 'blue'); } } } else { logs.addLog('奇怪的倍速,将会自动跳过视频任务', 'orange'); } } // 🚀 新增:优化功能按钮事件处理 top.document.getElementById('smartSpeedControl').onclick = function () { smartSpeedControl = !smartSpeedControl; GM_setValue('smartSpeedControl', smartSpeedControl); updateOptimizationButtonState('smartSpeedControl', smartSpeedControl); logs.addLog('智能倍速调节已' + (smartSpeedControl ? '开启' : '关闭'), smartSpeedControl ? 'green' : 'orange'); } top.document.getElementById('durationCompensation').onclick = function () { durationCompensation = !durationCompensation; GM_setValue('durationCompensation', durationCompensation); updateOptimizationButtonState('durationCompensation', durationCompensation); logs.addLog('时长补偿机制已' + (durationCompensation ? '开启' : '关闭'), durationCompensation ? 'green' : 'orange'); } top.document.getElementById('behaviorSimulation').onclick = function () { behaviorSimulation = !behaviorSimulation; GM_setValue('behaviorSimulation', behaviorSimulation); updateOptimizationButtonState('behaviorSimulation', behaviorSimulation); logs.addLog('行为模拟优化已' + (behaviorSimulation ? '开启' : '关闭'), behaviorSimulation ? 'green' : 'orange'); } top.document.getElementById('detectionEvasion').onclick = function () { detectionEvasion = !detectionEvasion; GM_setValue('detectionEvasion', detectionEvasion); updateOptimizationButtonState('detectionEvasion', detectionEvasion); logs.addLog('检测规避技术已' + (detectionEvasion ? '开启' : '关闭'), detectionEvasion ? 'green' : 'orange'); } top.document.getElementById('riskWarning').onclick = function () { riskWarning = !riskWarning; GM_setValue('riskWarning', riskWarning); updateOptimizationButtonState('riskWarning', riskWarning); logs.addLog('风险提示已' + (riskWarning ? '开启' : '关闭'), riskWarning ? 'green' : 'orange'); } top.document.getElementById('backGround').onclick = function () { let backGroundButton = top.document.getElementById('backGround'); if (backGroundButton.getAttribute('class') == 'btn btn-default') { // 开启挂机:切换为 btn-success(绿色样式) backGroundButton.setAttribute('class', 'btn btn-success'); logs.addLog('挂机已激活...', 'green'); // 日志文字也同步为绿色,呼应按钮颜色 _w.top.backNow = 1; GM_setValue('unrivalbackground', '1'); // 保存激活状态 } else { // 关闭挂机:恢复为 btn-default(默认白色样式) backGroundButton.setAttribute('class', 'btn btn-default'); logs.addLog('挂机已关闭...', 'green'); _w.top.backNow = 0; GM_setValue('unrivalbackground', '0'); // 保存取消激活状态 } } top.document.getElementById('reviewModeButton').onclick = function () { let reviewButton = top.document.getElementById('reviewModeButton'); if (reviewButton.getAttribute('class') == 'btn btn-default') { // 开启复习模式:切换为 btn-success(绿色样式) reviewButton.setAttribute('class', 'btn btn-success'); logs.addLog('复习模式已开启...', 'green'); // 日志文字也同步为绿色,呼应按钮颜色 GM_setValue('unrivalreview', '1'); _w.top.unrivalReviewMode = '1'; } else { // 关闭复习模式:恢复为 btn-default(默认白色样式) reviewButton.setAttribute('class', 'btn btn-default'); logs.addLog('复习模式已关闭...', 'green'); GM_setValue('unrivalreview', '0'); _w.top.unrivalReviewMode = '0'; } } top.document.getElementById('autoDoWorkButton').onclick = function () { let autoDoWorkButton = top.document.getElementById('autoDoWorkButton'); if (autoDoWorkButton.getAttribute('class') == 'btn btn-default') { // 开启自动答题:切换为 btn-success(绿色样式) autoDoWorkButton.setAttribute('class', 'btn btn-success'); logs.addLog('自动答题已开启...', 'green'); // 日志文字也同步为绿色,呼应按钮颜色 GM_setValue('unrivaldowork', '1'); _w.top.unrivalDoWork = '1'; } else { // 关闭自动答题:恢复为 btn-default(默认白色样式) autoDoWorkButton.setAttribute('class', 'btn btn-default'); logs.addLog('自动答题已关闭...', 'green'); GM_setValue('unrivaldowork', '0'); _w.top.unrivalDoWork = '0'; } } top.document.getElementById('autoSubmitButton').onclick = function () { let autoSubmitButton = top.document.getElementById('autoSubmitButton'); if (autoSubmitButton.getAttribute('class') == 'btn btn-default') { // 开启自动提交:切换为 btn-success(绿色样式) autoSubmitButton.setAttribute('class', 'btn btn-success'); logs.addLog('自动提交已开启...', 'green'); // 日志文字也同步为绿色,呼应按钮颜色 GM_setValue('unrivalautosubmit', '1'); _w.top.unrivalAutoSubmit = '1'; } else { // 关闭自动提交:恢复为 btn-default(默认白色样式) autoSubmitButton.setAttribute('class', 'btn btn-default'); logs.addLog('自动提交已关闭...', 'green'); GM_setValue('unrivalautosubmit', '0'); _w.top.unrivalAutoSubmit = '0'; } } top.document.getElementById('autoSaveButton').onclick = function () { let autoSaveButton = top.document.getElementById('autoSaveButton'); if (autoSaveButton.getAttribute('class') == 'btn btn-default') { // 开启自动保存:切换为 btn-success(绿色样式) autoSaveButton.setAttribute('class', 'btn btn-success'); logs.addLog('自动保存已开启...', 'green'); // 日志文字也同步为绿色,呼应按钮颜色 GM_setValue('unrivalautosave', '1'); _w.top.unrivalAutoSave = '1'; } else { // 关闭自动保存:恢复为 btn-default(默认白色样式) autoSaveButton.setAttribute('class', 'btn btn-default'); logs.addLog('自动保存已关闭...', 'green'); GM_setValue('unrivalautosave', '0'); _w.top.unrivalAutoSave = '0'; } } top.document.getElementById('videoTimeButton').onclick = function () { top.document.getElementById('videoTime').style.display = 'block'; top.document.getElementById('videoTimeContent').src = _p + '//stat2-ans.chaoxing.com/task/s/index?courseid=' + courseId + '&clazzid=' + classId; } //新增交互按钮 // 初始设置按钮状态和颜色 top.document.getElementById('jumpTypeButton').textContent = '模式: ' + (jumpType === 0 ? '智能' : jumpType === 1 ? '遍历' : '不跳转'); top.document.getElementById('disableMonitorButton').textContent = '多端学习: ' + (disableMonitor === 1 ? '解除' : '启用'); top.document.getElementById('autoLoginButton').textContent = '自动登录: ' + (autoLogin === 1 ? '开启' : '关闭'); top.document.getElementById('autoStartButton').textContent = '自动阅读: ' + (READING_CONFIG.autoStart ? '开启' : '关闭'); top.document.getElementById('videoQuestionButton').textContent = '视频题目: ' + (videoQuestionEnabled ? '开启' : '关闭'); top.document.getElementById('phoneNumber').value = GM_getValue('phoneNumber', ''); top.document.getElementById('password').value = GM_getValue('password', ''); // 设置初始按钮颜色 if (disableMonitor === 1) { top.document.getElementById('disableMonitorButton').setAttribute('class', 'btn btn-success'); } else { top.document.getElementById('disableMonitorButton').setAttribute('class', 'btn btn-default'); } if (autoLogin === 1) { top.document.getElementById('autoLoginButton').setAttribute('class', 'btn btn-success'); } else { top.document.getElementById('autoLoginButton').setAttribute('class', 'btn btn-default'); } if (READING_CONFIG.autoStart) { top.document.getElementById('autoStartButton').setAttribute('class', 'btn btn-success'); } else { top.document.getElementById('autoStartButton').setAttribute('class', 'btn btn-default'); } if (videoQuestionEnabled) { top.document.getElementById('videoQuestionButton').setAttribute('class', 'btn btn-success'); } else { top.document.getElementById('videoQuestionButton').setAttribute('class', 'btn btn-default'); } // jumpType切换事件 top.document.getElementById('jumpTypeButton').onclick = function () { jumpType = (jumpType + 1) % 3; let modeText = ['智能', '遍历', '不跳转'][jumpType]; this.textContent = '模式: ' + modeText; GM_setValue('jumpType', jumpType); logs.addLog('已切换到' + modeText + '模式', 'green'); }; // disableMonitor切换事件 top.document.getElementById('disableMonitorButton').onclick = function () { disableMonitor = (disableMonitor === 1) ? 0 : 1; let statusText = (disableMonitor === 1) ? '解除' : '启用'; this.textContent = '多端学习: ' + statusText; GM_setValue('disableMonitor', disableMonitor); if (disableMonitor === 1) { this.setAttribute('class', 'btn btn-success'); logs.addLog('多端学习已开启', 'green'); // 立即劫持appendChild,防止监控脚本注入 _w.appendChild = _w.Element.prototype.appendChild; _w.Element.prototype.appendChild = function () { try { if (arguments[0].src && arguments[0].src.indexOf('detect.chaoxing.com') > 0) { return; } } catch (e) { } _w.appendChild.apply(this, arguments); }; } else { this.setAttribute('class', 'btn btn-default'); logs.addLog('多端学习已关闭', 'green'); // 立即恢复原生appendChild if (_w.appendChild) { _w.Element.prototype.appendChild = _w.appendChild; } } }; // autoLogin切换事件 top.document.getElementById('autoLoginButton').onclick = function () { autoLogin = (autoLogin === 1) ? 0 : 1; this.textContent = '自动登录: ' + (autoLogin === 1 ? '开启' : '关闭'); GM_setValue('autoLogin', autoLogin); if (autoLogin === 1) { this.setAttribute('class', 'btn btn-success'); logs.addLog('自动登录功能已开启', 'green'); } else { this.setAttribute('class', 'btn btn-default'); logs.addLog('自动登录功能已关闭', 'green'); } }; // autoStart切换事件 top.document.getElementById('autoStartButton').onclick = function () { READING_CONFIG.autoStart = !READING_CONFIG.autoStart; this.textContent = '自动阅读: ' + (READING_CONFIG.autoStart ? '开启' : '关闭'); GM_setValue('autoStart', READING_CONFIG.autoStart); if (READING_CONFIG.autoStart) { this.setAttribute('class', 'btn btn-success'); logs.addLog('自动阅读功能已开启', 'green'); } else { this.setAttribute('class', 'btn btn-default'); logs.addLog('自动阅读功能已关闭', 'green'); } }; // videoQuestion切换事件 top.document.getElementById('videoQuestionButton').onclick = function () { videoQuestionEnabled = !videoQuestionEnabled; this.textContent = '视频题目: ' + (videoQuestionEnabled ? '开启' : '关闭'); GM_setValue('videoQuestionEnabled', videoQuestionEnabled); if (videoQuestionEnabled) { this.setAttribute('class', 'btn btn-success'); logs.addLog('视频题目自动处理功能已开启', 'green'); } else { this.setAttribute('class', 'btn btn-default'); logs.addLog('视频题目自动处理功能已关闭', 'green'); } }; // 保存登录信息事件 top.document.getElementById('saveLoginInfo').onclick = function () { let newPhone = top.document.getElementById('phoneNumber').value; let newPassword = top.document.getElementById('password').value; if (newPhone && !/^1[3-9]\d{9}$/.test(newPhone)) { logs.addLog('手机号格式错误,请重新输入', 'red'); return; } GM_setValue('phoneNumber', newPhone); GM_setValue('password', newPassword); phoneNumber = newPhone; password = newPassword; logs.addLog('登录信息已保存', 'green'); }; } }, 100), loopjob = () => { if (_w.top.unrivalScriptList.length > 1) { logs.addLog('您同时开启了多个刷课脚本,建议关闭其他脚本,否则会有挂科风险!', 'red'); } if (cVersion < 8.6 * 10) { logs.addLog( '\u60a8\u7684\u6d4f\u89c8\u5668\u5185\u6838\u8fc7\u8001\uff0c\u8bf7\u66f4\u65b0\u7248\u672c\u6216\u4f7f\u7528\u4e3b\u6d41\u6d4f\u89c8\u5668\uff0c\u63a8\u8350\u003c\u0061\u0020\u0068\u0072\u0065\u0066\u003d\u0022\u0068\u0074\u0074\u0070\u0073\u003a\u002f\u002f\u0077\u0077\u0077\u002e\u006d\u0069\u0063\u0072\u006f\u0073\u006f\u0066\u0074\u002e\u0063\u006f\u006d\u002f\u007a\u0068\u002d\u0063\u006e\u002f\u0065\u0064\u0067\u0065\u0022\u0020\u0074\u0061\u0072\u0067\u0065\u0074\u003d\u0022\u0076\u0069\u0065\u0077\u005f\u0077\u0069\u006e\u0064\u006f\u0077\u0022\u003e\u0065\u0064\u0067\u0065\u6d4f\u89c8\u5668\u003c\u002f\u0061\u003e', 'red'); stop = true; return; } if (stop) { return; } let missionli = missionList; if (missionli == []) { setTimeout(loopjob, 500); return; } for (let itemName in missionli) { if (missionli[itemName]['running']) { setTimeout(loopjob, 500); return; } } for (let itemName in missionli) { if (!missionli[itemName]['done']) { switch (missionli[itemName]['type']) { case 'video': doVideo(missionli[itemName]).catch(e => { logs.addLog('视频任务执行出错: ' + e.message, 'red'); }); break; case 'document': doDocument(missionli[itemName]); break; case 'work': doWork(missionli[itemName]); break; } setTimeout(loopjob, 500); return; } } if (busyThread <= 0) { if (jumpType != 2) { _w.top.jump = true; logs.addLog('🎉 所有任务处理完毕,5秒后自动下一章', 'green'); } else { logs.addLog('所有任务处理完毕,用户设置为不跳转,脚本已结束运行,如需自动跳转,请编辑脚本代码参数', 'green'); } clearInterval(loopjob); } else { setTimeout(loopjob, 500); } }, readyCheck = () => { setTimeout(function () { try { if (!isCat) { logs.addLog( '推荐使用脚本猫运行此脚本,使用其他脚本管理器不保证能正常运行', 'orange'); } if (_w.top.unrivalReviewMode == '1') { logs.addLog('复习模式已开启,遇到已完成的视频任务不会跳过', 'green'); top.document.getElementById('reviewModeButton').setAttribute('class', 'btn btn-success'); } else { top.document.getElementById('reviewModeButton').setAttribute('class', 'btn btn-default'); } var backGroundStatus = GM_getValue('unrivalbackground', '0'); if (backGroundStatus === '1') { _w.top.backNow = 1; top.document.getElementById('backGround').setAttribute('class', 'btn btn-success'); // 设置按钮为绿色 } else { top.document.getElementById('backGround').setAttribute('class', 'btn btn-default'); // 设置按钮为白色 } if (_w.top.unrivalDoWork == '1') { logs.addLog('自动做章节测试已开启,将会自动做章节测试', 'green'); top.document.getElementById('autoDoWorkButton').setAttribute('class', 'btn btn-success'); } else { top.document.getElementById('autoDoWorkButton').setAttribute('class', 'btn btn-default'); } if (_w.top.unrivalAutoSubmit == '1') { top.document.getElementById('autoSubmitButton').setAttribute('class', 'btn btn-success'); } else { top.document.getElementById('autoSubmitButton').setAttribute('class', 'btn btn-default'); } if (_w.top.unrivalAutoSave == '1') { top.document.getElementById('autoSaveButton').setAttribute('class', 'btn btn-success'); } else { top.document.getElementById('autoSaveButton').setAttribute('class', 'btn btn-default'); } } catch (e) { console.log(e); readyCheck(); return; } }, 500); } readyCheck(); try { var pageData = JSON.parse(param); } catch (e) { if (jumpType != 2) { _w.top.jump = true; logs.addLog('ℹ️ 此页无任务,5秒后自动下一章', 'blue'); } else { logs.addLog('此页无任务,用户设置为不跳转,脚本已结束运行,如需自动跳转,请编辑脚本代码参数', 'green'); } return; } var data = pageData['defaults'], jobList = [], classId = data['clazzId'], chapterId = data['knowledgeid'], reportUrl = data['reportUrl'], ktoken = data['ktoken']; UID = UID || data['userid']; FID = FID || data['fid']; for (let i = 0, l = pageData['attachments'].length; i < l; i++) { let item = pageData['attachments'][i]; if (item['job'] != true || item['isPassed'] == true) { if (_w.top.unrivalReviewMode == '1' && item['type'] == 'video') { jobList.push(item); } continue; } else { jobList.push(item); } } var video_getReady = (item) => { let statusUrl = _p + '//' + _h + '/ananas/status/' + item['property']['objectid'] + '?k=' + FID + '&flag=normal&_dc=' + String(Math.round(new Date())), doubleSpeed = item['property']['doublespeed']; busyThread += 1; GM_xmlhttpRequest({ method: "get", headers: { 'Host': _h, 'Referer': vrefer, 'Sec-Fetch-Site': 'same-origin' }, url: statusUrl, onload: function (res) { try { busyThread -= 1; let videoInfo = JSON.parse(res.responseText), duration = videoInfo['duration'], dtoken = videoInfo['dtoken']; if (duration == undefined) { top.document.getElementById('joblist').innerHTML += `
` + '[无效视频]' + item['property']['name'] + `
` return; } missionList['m' + item['jobid']] = { 'module': item['property']['module'], 'type': 'video', 'dtoken': dtoken, 'duration': duration, 'objectId': item['property']['objectid'], 'rt': item['property']['rt'] || '0.9', 'otherInfo': item['otherInfo'], 'doublespeed': doubleSpeed, 'jobid': item['jobid'], 'name': item['property']['name'], 'done': false, 'running': false }; top.document.getElementById('joblist').innerHTML += `
` + '[视频]' + item['property']['name'] + `
` } catch (e) { } }, onerror: function (err) { console.log(err); if (err.error.indexOf('@connect list') >= 0) { logs.addLog('请添加安全网址,将 【 //@connect ' + _h + ' 】方括号里的内容(不包括方括号)添加到脚本代码内指定位置,否则脚本无法正常运行,如图所示:', 'red'); logs.addLog( '' ); stop = true; } else { logs.addLog('获取任务详情失败', 'red'); logs.addLog('错误原因:' + err.error, 'red'); } } }); }, // 视频题目处理相关函数 - 完整适配用户建议脚本 initVideoQuestions = async (mid, cpi, classid) => { return new Promise((resolve, reject) => { let url = `${_p}//${_h}/ananas/initdatawithviewer?mid=${mid}&cpi=${cpi}&classid=${classid}&_dc=${new Date().valueOf()}`; GM_xmlhttpRequest({ method: "get", url: url, headers: { 'Host': _h, 'Referer': vrefer, 'Sec-Fetch-Site': 'same-origin', 'User-Agent': navigator.userAgent }, timeout: 10000, onload: function(res) { try { if (res.status === 200) { let data = JSON.parse(res.responseText); resolve(data); } else { reject(new Error(`HTTP ${res.status}`)); } } catch (e) { reject(e); } }, onerror: function(err) { if (err.error && err.error.includes("connect list")) { const domain = err.error.match(/:\/\/(.[^/]+)/)[1]; const notice = `由于connect未添加导致无权限请求

请复制以下代码至脚本中的第19行位置
// @connect ${domain}`; logs.addLog(notice, 'red'); } else { logs.addLog(`请求报错[${url} - GET][${err.statusText || 'timeout'}]`, 'red'); } reject(err); }, ontimeout: function() { logs.addLog(`请求报错[${url} - GET][timeout]`, 'red'); reject(new Error('Request timeout')); } }); }); }, submitVideoAnswer = async (classid, cpi, objectid, eventid, memberinfo, answer) => { return new Promise((resolve, reject) => { let url = `${_p}//${_h}/question/quiz-validation?classid=${classid}&cpi=${cpi}&objectid=${objectid}&_dc=${new Date().valueOf()}&eventid=${eventid}&memberinfo=${memberinfo}&answerContent=${answer}`; GM_xmlhttpRequest({ method: "get", url: url, headers: { 'Host': _h, 'Referer': vrefer, 'Sec-Fetch-Site': 'same-origin', 'User-Agent': navigator.userAgent }, timeout: 10000, onload: function(res) { try { if (res.status === 200) { let data = JSON.parse(res.responseText); resolve(data); } else { reject(new Error(`HTTP ${res.status}`)); } } catch (e) { reject(e); } }, onerror: function(err) { if (err.error && err.error.includes("connect list")) { const domain = err.error.match(/:\/\/(.[^/]+)/)[1]; const notice = `由于connect未添加导致无权限请求

请复制以下代码至脚本中的第19行位置
// @connect ${domain}`; logs.addLog(notice, 'red'); } else { logs.addLog(`请求报错[${url} - GET][${err.statusText || 'timeout'}]`, 'red'); } reject(err); }, ontimeout: function() { logs.addLog(`请求报错[${url} - GET][timeout]`, 'red'); reject(new Error('Request timeout')); } }); }); }, // 分析视频任务失败原因 analyzeVideoTaskFailure = (item, playTime, duration, isdrag, ispass) => { let reasons = []; let suggestions = []; // 1. 检查视频题目处理状态 if (videoQuestionEnabled) { reasons.push("视频题目:已启用自动处理 ✅"); // 检查是否有题目处理记录 let questionLogs = logs.getLogs().filter(log => log.includes('正在完成视频中的题目') || log.includes('视频内无提问') || log.includes('视频题目已完毕') ); if (questionLogs.length > 0) { reasons.push("题目处理:有处理记录 ✅"); } else { reasons.push("题目处理:无处理记录 ⚠️"); suggestions.push("检查视频是否真的有题目"); } } else { reasons.push("视频题目:未启用自动处理 ⚠️"); suggestions.push("建议开启视频题目自动处理功能"); } // 2. 检查学习时长 let durationPercent = (playTime / duration * 100).toFixed(1); if (playTime >= duration) { reasons.push(`学习时长:${playTime}秒 (要求:${duration}秒) ${durationPercent}% ✅`); } else { reasons.push(`学习时长:${playTime}秒 (要求:${duration}秒) ${durationPercent}% ❌`); suggestions.push("增加播放时长"); } // 3. 检查倍速设置 if (rate > 3) { reasons.push(`播放倍速:${rate}倍 (高风险) ⚠️⚠️`); suggestions.push("降低倍速到1-2倍"); if (riskWarning) { suggestions.push("建议开启智能倍速调节和检测规避技术"); } } else if (rate > 2) { reasons.push(`播放倍速:${rate}倍 (可能被检测) ⚠️`); suggestions.push("建议降低倍速到1-2倍"); if (riskWarning) { suggestions.push("建议开启优化功能降低风险"); } } else { reasons.push(`播放倍速:${rate}倍 ✅`); } // 🚀 新增:优化功能状态检查 if (riskWarning) { const enabledFeatures = []; if (smartSpeedControl) enabledFeatures.push("智能倍速调节"); if (durationCompensation) enabledFeatures.push("时长补偿"); if (behaviorSimulation) enabledFeatures.push("行为模拟"); if (detectionEvasion) enabledFeatures.push("检测规避"); if (enabledFeatures.length > 0) { reasons.push(`优化功能:${enabledFeatures.join("、")} ✅`); } else { reasons.push("优化功能:未开启 ⚠️"); suggestions.push("建议开启优化功能以提高安全性"); } } // 4. 检查服务器响应状态 if (isdrag === '4') { reasons.push("服务器状态:isdrag=4 (时长足够但未确认完成)"); suggestions.push("开启复习模式重新播放"); } // 5. 检查任务通过状态 if (ispass) { if (ispass.isPassed === true) { reasons.push("任务状态:已通过验证 ✅"); } else { reasons.push("任务状态:未通过验证 ❌"); suggestions.push("检查是否有遗漏的交互要求"); } } else { reasons.push("任务状态:未知状态 ⚠️"); } // 6. 检查复习模式 if (_w.top.unrivalReviewMode === '1') { reasons.push("当前模式:复习模式 ✅"); } else { reasons.push("当前模式:普通模式"); suggestions.push("尝试开启复习模式"); } // 7. 检查网络状态 reasons.push("网络状态:正常 ✅"); // 8. 添加通用建议 if (suggestions.length === 0) { suggestions.push("1. 开启复习模式"); suggestions.push("2. 使用1倍速重新播放"); suggestions.push("3. 如果仍不通过,手动点击视频确认完成"); } return { reasons: reasons, suggestions: suggestions }; }, finishVideoQuestions = async (item) => { try { logs.addLog(`[${item['name']}]获取视频中的题目`, 'info'); // 检查视频对象结构 if (!item) { logs.addLog(`[${item['name']}]视频对象为空`, 'error'); return false; } if (!item['property']) { logs.addLog(`[${item['name']}]视频property属性不存在`, 'error'); logs.addLog(`视频对象结构: ${JSON.stringify(item, null, 2)}`, 'info'); logs.addLog(`[${item['name']}]视频内无提问`, 'green'); return true; } if (!item['property']['mid']) { logs.addLog(`[${item['name']}]视频mid属性不存在`, 'error'); logs.addLog(`property结构: ${JSON.stringify(item['property'], null, 2)}`, 'info'); logs.addLog(`[${item['name']}]视频内无提问`, 'green'); return true; } // 获取视频题目 let res = await initVideoQuestions(item['property']['mid'], UID, classId); if (!res || res.length === 0) { logs.addLog(`[${item['name']}]视频题目已完毕`, 'info'); return true; } // 处理每道题目 for (const questionItem of res) { try { if (!questionItem.datas || questionItem.datas.length === 0) { logs.addLog("有个垃圾题跳过", 'error'); continue; } const item1 = questionItem.datas[0]; if (!item1.options) { logs.addLog("有个垃圾题跳过", 'error'); continue; } const options = item1.options; // 获取正确答案 let answer = options.filter(option => option.isRight == true).map(option => option.name).join(); if (!answer) { logs.addLog("有个垃圾题跳过", 'error'); continue; } // 提交答案 let res1 = await submitVideoAnswer(classId, UID, item['objectId'], item1.resourceId, item1.memberinfo, answer); if (res1.status) { logs.addLog(`[正在完成视频中的题目]:${item1.description}
答案:${answer}
${res1.isRight ? "答案正确" : "答案错误"}`, 'success'); } else { logs.addLog(`[正在完成视频中的题目]:${item1.description}
答案:${answer}
${res1.msg}`, 'error'); } // 等待一段时间再处理下一题 await new Promise(resolve => setTimeout(resolve, 1000)); } catch (e) { logs.addLog("有个垃圾题跳过", 'error'); } } logs.addLog(`[${item['name']}]视频题目已完毕`, 'info'); return true; } catch (e) { logs.addLog(`[${item['name']}]处理视频题目时出错: ${e.message}`, 'red'); logs.addLog(`视频对象: ${JSON.stringify(item, null, 2)}`, 'info'); return false; } }, doVideo = async (item) => { if (rate <= 0) { missionList['m' + item['jobid']]['running'] = true; logs.addLog('⚠️ 奇怪的倍速,视频已自动跳过', 'orange'); setTimeout(function () { missionList['m' + item['jobid']]['running'] = false; missionList['m' + item['jobid']]['done'] = true; }, 5000); return; } if (allowBackground && backGround) { if (_w.top.document.getElementsByClassName('catalog_points_sa').length > 0 || _w.top.document .getElementsByClassName('lock').length > 0) { logs.addLog('您已安装超星挂机小助手,但此课程可能为闯关模式,不支持后台挂机,将为您在线完成', 'blue'); } else { item['userid'] = UID; item['classId'] = classId; item['review'] = [false, true][_w.top.unrivalReviewMode]; item['reportUrl'] = reportUrl; item['rt'] = missionList['m' + item['jobid']]['rt']; GM_setValue('unrivalBackgroundVideo', item); _d.cookie = "videojs_id=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; logs.addLog( '您已安装超星挂机小助手,已添加至后台任务,点我查看后台', 'green'); missionList['m' + item['jobid']]['running'] = true; setTimeout(function () { missionList['m' + item['jobid']]['running'] = false; missionList['m' + item['jobid']]['done'] = true; }, 5000); return; } } let videojs_id = String(parseInt(Math.random() * 9999999)); _d.cookie = 'videojs_id=' + videojs_id + ';path=/' // 先处理视频中的题目(如果启用) if (videoQuestionEnabled) { logs.addLog('开始处理视频题目:' + item['name'], 'blue'); try { await finishVideoQuestions(item); } catch (e) { logs.addLog('处理视频题目时出错,继续播放视频:' + e.message, 'orange'); } } else { logs.addLog('视频题目自动处理功能已关闭,跳过题目处理', 'info'); } logs.addLog('🎬 开始刷视频:' + item['name'] + ',倍速:' + String(rate) + '倍', 'blue'); logs.addLog('视频观看信息每60秒上报一次,请耐心等待', 'green'); logs.addLog('如遇脚本使用异常情况,请检查脚本版本是否为最新版,点我(脚本猫)点我(greasyfork)检查', 'orange'); if (disableMonitor) { logs.addLog('解除多端学习监控有清除进度风险,请谨慎使用', 'orange'); } let dtype = 'Video'; if (item['module'].includes('audio')) { dtype = 'Audio'; rt = ''; } let playTime = 0, playsTime = 0, isdrag = '3', times = 0, encUrl = '', first = true, loop = setInterval(function () { if (rate <= 0) { clearInterval(loop); logs.addLog('⚠️ 奇怪的倍速,视频已自动跳过', 'orange'); setTimeout(function () { missionList['m' + item['jobid']]['running'] = false; missionList['m' + item['jobid']]['done'] = true; }, 5000); return; } else if (item['doublespeed'] == 0 && rate > 1 && _w.top.unrivalReviewMode == '0') { //rate = 1; //logs.addLog('该视频不允许倍速播放,已恢复至一倍速,高倍速会被清空进度挂科,勿存侥幸', 'red'); } // 🚀 应用优化功能 let effectiveRate = rate; // 1. 检测规避技术 effectiveRate = getEvasionRate(effectiveRate); // 2. 智能倍速调节 effectiveRate = getSmartSpeed(effectiveRate); // 3. 行为模拟 simulateUserBehavior(); rt = missionList['m' + item['jobid']]['rt']; playsTime += effectiveRate; playTime = Math.ceil(playsTime); // 4. 时长补偿机制 playTime = compensateDuration(playTime, item['duration'], rate); if (times == 0 || times % 30 == 0 || playTime >= item['duration']) { if (first) { playTime = 0; } if (playTime >= item['duration']) { clearInterval(loop); playTime = item['duration']; isdrag = '4'; } else if (playTime > 0) { isdrag = '0'; } encUrl = host + 'chaoXing/v3/getEnc.php?classid=' + classId + '&playtime=' + playTime + '&duration=' + item['duration'] + '&objectid=' + item[ 'objectId'] + '&jobid=' + item['jobid'] + '&uid=' + UID; busyThread += 1; var _bold_playTime = playTime; function ecOnload(res) { let enc = ''; if (res && res.status == 200) { enc = res.responseText; if (enc.includes('--#')) { let warnInfo = enc.match(new RegExp('--#(.*?)--#', "ig"))[0] .replace(/--#/ig, ''); logs.addLog(warnInfo, 'red'); enc = enc.replace(/--#(.*?)--#/ig, ''); } if (enc.indexOf('.stop') >= 0) { clearInterval(loop); stop = true; return; } } else { strEc = `[${classId}][${UID}][${item['jobid']}][${item['objectId']}][${playTime * 1000}][d_yHJ!$pdA~5][${item['duration'] * 1000}][0_${item['duration']}]`, enc = jq.md5(strEc); } if (enc.length != 32) { clearInterval(loop); stop = true; return; } let reportsUrl = reportUrl + '/' + item['dtoken'] + '?clazzId=' + classId + '&playingTime=' + playTime + '&duration=' + item['duration'] + '&clipTime=0_' + item[ 'duration'] + '&objectId=' + item['objectId'] + '&otherInfo=' + item['otherInfo'] + '&jobid=' + item[ 'jobid'] + '&userid=' + UID + '&isdrag=' + isdrag + '&view=pc&enc=' + enc + '&rt=' + rt + '&dtype=' + dtype + '&_t=' + String(Math.round(new Date())); GM_xmlhttpRequest({ method: "get", headers: { 'Host': _h, 'Referer': vrefer, 'Sec-Fetch-Site': 'same-origin', 'Content-Type': 'application/json' }, url: reportsUrl, onload: function (res) { try { let today = new Date(), todayStr = today.getFullYear() + 'd' + today.getMonth() + 'd' + today .getDate(), timelong = GM_getValue( 'unrivaltimelong', {}); if (timelong[UID] == undefined || timelong[UID]['today'] != todayStr ) { timelong[UID] = { 'time': 0, 'today': todayStr }; } else { timelong[UID]['time']++; } GM_setValue('unrivaltimelong', timelong); busyThread -= 1; if (timelong[UID]['time'] / 60 > 22 && item['doublespeed'] == 0 && _w.top .unrivalReviewMode == '0') { clearInterval(loop); logs.addLog( '今日学习时间过长,继续学习会导致清空进度,请明天再来', 'red'); setTimeout(function () { missionList['m' + item[ 'jobid']][ 'running' ] = false; missionList['m' + item[ 'jobid']][ 'done' ] = true; }, 5000); return; } let ispass = JSON.parse(res .responseText); first = false; if (ispass['isPassed'] && _w.top .unrivalReviewMode == '0') { logs.addLog('✅ 视频任务已完成', 'green'); missionList['m' + item['jobid']]['running'] = false; missionList['m' + item['jobid']]['done'] = true; clearInterval(loop); return; } else if (isdrag == '4') { if (_w.top.unrivalReviewMode == '1') { logs.addLog('✅ 视频已观看完毕', 'green'); } else { // 分析任务失败原因 let analysis = analyzeVideoTaskFailure(item, playTime, item['duration'], isdrag, ispass); // 生成详细的原因说明 let mainReason = ''; if (analysis.reasons.some(r => r.includes('视频题目'))) { mainReason = ' - 可能原因:视频内提问未完成或学习通服务器未通过'; } else if (analysis.reasons.some(r => r.includes('观看时长'))) { mainReason = ' - 可能原因:观看时长不足或倍速过高'; } else if (analysis.reasons.some(r => r.includes('服务器'))) { mainReason = ' - 可能原因:学习通服务器未通过或网络问题'; } else { mainReason = ' - 可能原因:学习通服务器未通过'; } logs.addLog(`[${item['name']}]视频已观看完毕,但视频任务未完成${mainReason}`, 'red'); // 只显示关键原因,避免重复 let keyReasons = analysis.reasons.filter(r => r.includes('❌') || r.includes('⚠️⚠️') || r.includes('未通过') ); if (keyReasons.length > 0) { logs.addLog('🔍 关键问题:', 'orange'); keyReasons.forEach(reason => { logs.addLog(`├─ ${reason}`, 'orange'); }); } // 只显示最重要的建议 if (analysis.suggestions.length > 0) { logs.addLog('💡 建议操作:', 'blue'); analysis.suggestions.slice(0, 3).forEach((suggestion, index) => { logs.addLog(`${index + 1}. ${suggestion}`, 'blue'); }); } } missionList['m' + item['jobid']][ 'running' ] = false; missionList['m' + item['jobid']][ 'done' ] = true; try { clearInterval(loop); } catch (e) { } } else { logs.addLog(item['name'] + '已观看' + _bold_playTime + '秒,剩余大约' + String(item['duration'] - _bold_playTime) + '秒'); } } catch (e) { console.log(e); if (res.responseText.indexOf('验证码') >= 0) { logs.addLog('已被超星风控,请点我处理,60秒后自动刷新页面', 'red'); missionList['m' + item['jobid']][ 'running' ] = false; clearInterval(loop); stop = true; setTimeout(function () { _l.reload(); }, 60000); return; } logs.addLog('超星返回错误信息,十秒后重试,请重新登录或重新打开浏览器', 'red'); times = -10; return; } }, onerror: function (err) { console.log(err); if (err.error.indexOf('@connect list') >= 0) { logs.addLog( '请添加安全网址,将 【 //@connect ' + _h + ' 】方括号里的内容(不包括方括号)添加到脚本代码内指定位置,否则脚本无法正常运行,如图所示:', 'red'); logs.addLog( '' ); stop = true; } else { logs.addLog('观看视频失败', 'red'); logs.addLog('错误原因:' + err.error, 'red'); } missionList['m' + item['jobid']][ 'running' ] = false; clearInterval(loop); } }); }; GM_xmlhttpRequest({ method: "get", url: encUrl, timeout: 2000, onload: ecOnload, onerror: function (err) { console.log(err); ecOnload(false); }, ontimeout: function (err) { console.log(err); ecOnload(false); } }); } times += 1; }, 1000); missionList['m' + item['jobid']]['running'] = true; }, doDocument = (item) => { missionList['m' + item['jobid']]['running'] = true; logs.addLog('开始刷文档:' + item['name']); setTimeout(function () { busyThread += 1; GM_xmlhttpRequest({ method: "get", url: _p + '//' + _h + '/ananas/job/document?jobid=' + item['jobid'] + '&knowledgeid=' + chapterId + '&courseid=' + courseId + '&clazzid=' + classId + '&jtoken=' + item['jtoken'], onload: function (res) { try { busyThread -= 1; let ispass = JSON.parse(res.responseText); if (ispass['status']) { logs.addLog('文档任务已完成', 'green'); } else { logs.addLog('文档已阅读完成,但任务点未完成', 'red'); } } catch (err) { console.log(err); console.log(res.responseText); logs.addLog('解析文档内容失败', 'red'); } missionList['m' + item['jobid']]['running'] = false; missionList['m' + item['jobid']]['done'] = true; }, onerror: function (err) { console.log(err); if (err.error.indexOf('@connect list') >= 0) { logs.addLog('请添加安全网址,将 【 //@connect ' + _h + ' 】方括号里的内容(不包括方括号)添加到脚本代码内指定位置,否则脚本无法正常运行,如图所示:', 'red'); logs.addLog( '' ); stop = true; } else { logs.addLog('阅读文档失败', 'red'); logs.addLog('错误原因:' + err.error, 'red'); } missionList['m' + item['jobid']]['running'] = false; missionList['m' + item['jobid']]['done'] = true; } }); }, parseInt(Math.random() * 2000 + 9000, 10)) }, doWork = (item) => { missionList['m' + item['jobid']]['running'] = true; logs.addLog('开始刷章节测试:' + item['name']); logs.addLog('您设置的答题正确率为:' + String(accuracy) + '%,只有在高于此正确率时才会提交测试', 'blue'); top.document.getElementById('workPanel').style.display = 'block'; top.document.getElementById('frame_content').src = _p + '//' + _h + '/work/phone/work?workId=' + item['jobid'] .replace('work-', '') + '&courseId=' + courseId + '&clazzId=' + classId + '&knowledgeId=' + chapterId + '&jobId=' + item['jobid'] + '&enc=' + item['enc']; _w.top.unrivalWorkInfo = ''; _w.top.unrivalDoneWorkId = ''; setInterval(function () { if (_w.top.unrivalWorkInfo != '') { logs.addLog(_w.top.unrivalWorkInfo); _w.top.unrivalWorkInfo = ''; } }, 100); let checkcross = setInterval(function () { if (_w.top.unrivalWorkDone == false) { clearInterval(checkcross); return; } let ifW = top.document.getElementById('frame_content').contentWindow; try { ifW.location.href; } catch (e) { console.log(e); if (e.message.indexOf('cross-origin') != -1) { clearInterval(checkcross); _w.top.unrivalWorkDone = true; return; } } }, 2000); let workDoneInterval = setInterval(function () { if (_w.top.unrivalWorkDone) { _w.top.unrivalWorkDone = false; clearInterval(workDoneInterval); _w.top.unrivalDoneWorkId = ''; top.document.getElementById('workPanel').style.display = 'none'; top.document.getElementById('frame_content').src = ''; setTimeout(function () { missionList['m' + item['jobid']]['running'] = false; missionList['m' + item['jobid']]['done'] = true; }, 5000); } }, 500); }, missionList = []; if (jobList.length <= 0) { if (jumpType != 2) { _w.top.jump = true; logs.addLog('ℹ️ 此页无任务,5秒后自动下一章', 'blue'); } else { logs.addLog('此页无任务,用户设置为不跳转,脚本已结束运行,如需自动跳转,请编辑脚本代码参数', 'green'); } return; } for (let i = 0, l = jobList.length; i < l; i++) { let item = jobList[i]; if (item['type'] == 'video') { video_getReady(item); } else if (item['type'] == 'document') { missionList['m' + item['jobid']] = { 'type': 'document', 'jtoken': item['jtoken'], 'jobid': item['jobid'], 'name': item['property']['name'], 'done': false, 'running': false }; top.document.getElementById('joblist').innerHTML += `
` + '[文档]' + item['property']['name'] + `
` } else if (item['type'] == 'workid' && _w.top.unrivalDoWork == '1') { missionList['m' + item['jobid']] = { 'type': 'work', 'workid': item['property']['workid'], 'jobid': item['jobid'], 'name': item['property']['title'], 'enc': item['enc'], 'done': false, 'running': false }; top.document.getElementById('joblist').innerHTML += `
` + '[章节测试]' + item['property']['title'] + `
` } else { try { let jobName = item['property']['name']; if (jobName == undefined) { jobName = item['property']['title']; } top.document.getElementById('joblist').innerHTML += `
` + '已跳过:' + jobName + `
` } catch (e) { } } } loopjob(); } else if (_l.href.includes("mycourse/studentstudy")) { var audiofile = '', audioPlayer = new Audio(audiofile); _w.top.backNow = 0; audioPlayer.loop = true; _w.audioPlayer = audioPlayer; setInterval(function () { try { _w.jQuery.fn.viewer.Constructor.prototype.show = () => { }; } catch (e) { } }, 1000); try { _w.unrivalScriptList.push('Fuck me please'); } catch (e) { _w.unrivalScriptList = ['Fuck me please']; } function checkOffline() { let dleft = _d.getElementsByClassName('left'); if (dleft.length == 1) { let img = dleft[0].getElementsByTagName('img'); if (img.length == 1) { if (img[0].src.indexOf('loading.gif') != -1) { return true; } } } return false; } setInterval(function () { if (checkOffline()) { setTimeout(function () { if (checkOffline()) { _l.reload(); } }, 10000) } }, 3000); _d.addEventListener('visibilitychange', function () { var c = 0; if (_w.top.backNow == 0) { _d.title = '⚠️请先激活挂机'; return } else { _d.title = '学生学习页面'; } if (_d.hidden) { audioPlayer.play(); var timer = setInterval(function () { if (c) { _d.title = '挂机中'; c = 0; } else { _d.title = '挂机中'; c = 1; } if (!_d.hidden) { clearInterval(timer); _d.title = '学生学习页面'; } }, 1300); } else { audioPlayer.pause(); } }); _w.unrivalgetTeacherAjax = _w.getTeacherAjax; _w.getTeacherAjax = (courseid, classid, cid) => { if (cid == getQueryVariable('chapterId')) { return; } _w.top.unrivalPageRd = ''; _w.unrivalgetTeacherAjax(courseid, classid, cid); } if (disableMonitor == 1) { _w.appendChild = _w.Element.prototype.appendChild; _w.Element.prototype.appendChild = function () { try { if (arguments[0].src.indexOf('detect.chaoxing.com') > 0) { return; } } catch (e) { } _w.appendChild.apply(this, arguments); }; } _w.jump = false; setInterval(function () { if (getQueryVariable('mooc2') == '1') { let tabs = _d.getElementsByClassName('posCatalog_select'); for (let i = 0, l = tabs.length; i < l; i++) { let tabId = tabs[i].getAttribute('id'); if (tabId.indexOf('cur') >= 0 && tabs[i].getAttribute('class') == 'posCatalog_select') { tabs[i].setAttribute('onclick', "getTeacherAjax('" + courseId + "','" + classId + "','" + tabId.replace('cur', '') + "');"); } } } else { let h4s = _d.getElementsByTagName('h4'), h5s = _d.getElementsByTagName('h5'); for (let i = 0, l = h4s.length; i < l; i++) { if (h4s[i].getAttribute('id').indexOf('cur') >= 0) { h4s[i].setAttribute('onclick', "getTeacherAjax('" + courseId + "','" + classId + "','" + h4s[i].getAttribute('id').replace('cur', '') + "');"); } } for (let i = 0, l = h5s.length; i < l; i++) { if (h5s[i].getAttribute('id').indexOf('cur') >= 0) { h5s[i].setAttribute('onclick', "getTeacherAjax('" + courseId + "','" + classId + "','" + h5s[i].getAttribute('id').replace('cur', '') + "');"); } } } }, 1000); setInterval(function () { let but = null; if (_w.jump) { _w.jump = false; _w.top.unrivalDoneWorkId = ''; _w.jjump = (rd) => { if (rd != _w.top.unrivalPageRd) { return; } try { setTimeout(function () { if (jumpType == 1) { if (getQueryVariable('mooc2') == '1') { but = _d.getElementsByClassName( 'jb_btn jb_btn_92 fs14 prev_next next'); } else { but = _d.getElementsByClassName('orientationright'); } try { setTimeout(function () { if (rd != _w.top.unrivalPageRd) { return; } but[0].click(); }, 2000); } catch (e) { } return; } if (getQueryVariable('mooc2') == '1') { let ul = _d.getElementsByClassName('prev_ul')[0], lis = ul.getElementsByTagName('li'); for (let i = 0, l = lis.length; i < l; i++) { if (lis[i].getAttribute('class') == 'active') { if (i + 1 >= l) { break; } else { try { lis[i + 1].click(); } catch (e) { } return; } } } let tabs = _d.getElementsByClassName('posCatalog_select'); for (let i = 0, l = tabs.length; i < l; i++) { if (tabs[i].getAttribute('class') == 'posCatalog_select posCatalog_active') { while (i + 1 < tabs.length) { let nextTab = tabs[i + 1]; if ((nextTab.innerHTML.includes( 'icon_Completed prevTips') && _w.top .unrivalReviewMode == '0') || nextTab .innerHTML.includes( 'catalog_points_er prevTips')) { i++; continue; } if (nextTab.id.indexOf('cur') < 0) { i++; continue; } let clickF = setInterval(function () { if (rd != _w.top.unrivalPageRd) { clearInterval(clickF); return; } nextTab.click(); }, 2000); break; } break; } } } else { let div = _d.getElementsByClassName('tabtags')[0], spans = div.getElementsByTagName('span'); for (let i = 0, l = spans.length; i < l; i++) { if (spans[i].getAttribute('class').indexOf('currents') >= 0) { if (i + 1 == l) { break; } else { try { spans[i + 1].click(); } catch (e) { } return; } } } let tabs = _d.getElementsByTagName('span'), newTabs = []; for (let i = 0, l = tabs.length; i < l; i++) { if (tabs[i].getAttribute('style') != null && tabs[i] .getAttribute('style').indexOf( 'cursor:pointer;height:18px;') >= 0) { newTabs.push(tabs[i]); } } tabs = newTabs; for (let i = 0, l = tabs.length; i < l; i++) { if (tabs[i].parentNode.getAttribute('class') == 'currents') { while (i + 1 < tabs.length) { let nextTab = tabs[i + 1].parentNode; if ((nextTab.innerHTML.includes( 'roundpoint blue') && _w.top .unrivalReviewMode == '0') || nextTab .innerHTML.includes('roundpointStudent lock') ) { i++; continue; } if (nextTab.id.indexOf('cur') < 0) { i++; continue; } let clickF = setInterval(function () { if (rd != _w.top.unrivalPageRd) { clearInterval(clickF); return; } nextTab.click(); }, 2000); break; } break; } } } }, 2000); } catch (e) { } } _w.onReadComplete1(); setTimeout('jjump("' + _w.top.unrivalPageRd + '")', 2856); } }, 200); } else if (_l.href.indexOf("work/phone/doHomeWork") > 0) { var wIdE = _d.getElementById('workLibraryId') || _d.getElementById('oldWorkId'), wid = wIdE.value; _w.top.unrivalWorkDone = false; _w.aalert = _w.alert; _w.alert = (msg) => { if (msg == '保存成功') { _w.top.unrivalDoneWorkId = getQueryVariable('workId'); return; } aalert(msg); } if (_w.top.unrivalDoneWorkId == getQueryVariable('workId')) { _w.top.unrivalWorkDone = true; return; } _w.confirm = (msg) => { return true; } var questionList = [], questionsElement = _d.getElementsByClassName('Py-mian1'), questionNum = questionsElement.length, totalQuestionNum = questionNum; for (let i = 0; i < questionNum; i++) { let questionElement = questionsElement[i], idElements = questionElement.getElementsByTagName('input'), questionId = '0', question = questionElement.getElementsByClassName('Py-m1-title fs16')[0].innerHTML; question = handleImgs(question).replace(/(<([^>]+)>)/ig, '').replace(/[0-9]{1,3}.\[(.*?)\]/ig, '').replaceAll('\n', '').replace(/^\s+/ig, '').replace(/\s+$/ig, ''); for (let z = 0, k = idElements.length; z < k; z++) { try { if (idElements[z].getAttribute('name').indexOf('answer') >= 0) { questionId = idElements[z].getAttribute('name').replace('type', ''); break; } } catch (e) { console.log(e); continue; } } if (questionId == '0' || question == '') { continue; } typeE = questionElement.getElementsByTagName('input'); if (typeE == null || typeE == []) { continue; } let typeN = 'fuckme'; for (let g = 0, h = typeE.length; g < h; g++) { if (typeE[g].id == 'answertype' + questionId.replace('answer', '').replace('check', '')) { typeN = typeE[g].value; break; } } if (['0', '1', '3'].indexOf(typeN) < 0) { continue; } type = { '0': '单选题', '1': '多选题', '3': '判断题' }[typeN]; let optionList = { length: 0 }; if (['单选题', '多选题'].indexOf(type) >= 0) { let answersElements = questionElement.getElementsByClassName('answerList')[0].getElementsByTagName( 'li'); for (let x = 0, j = answersElements.length; x < j; x++) { let optionE = answersElements[x], optionTextE = trim(optionE.innerHTML.replace(/(^\s*)|(\s*$)/g, "")), optionText = optionTextE.slice(1).replace(/(^\s*)|(\s*$)/g, ""), optionValue = optionTextE.slice(0, 1), optionId = optionE.getAttribute('id-param'); if (optionText == '') { break; } optionList[optionText] = { 'id': optionId, 'value': optionValue } optionList.length++; } if (answersElements.length != optionList.length) { continue; } } questionList.push({ 'question': question, 'type': type, 'questionid': questionId, 'options': optionList }); } var qu = null, nowTime = -4000, busyThread = questionList.length, ctOnload = function (res, quu) { busyThread -= 1; var ctResult = { 'code': -1, 'finalUrl': '', 'data': '未找到答案,建议使用AI作答(https://studyai0.com/' }; if (res) { try { var responseText = res.responseText, ctResult = JSON.parse(responseText); } catch (e) { console.log(e); if (res.finalUrl.includes('getAnswer.php')) { _w.top.unrivalWorkInfo = '查题错误,服务器连接失败(使用高峰期),等待一段时间'; return; } } } try { let choiceEs = _d.getElementsByTagName('li'); if (ctResult['code'] == -1 ) { try { if (ctResult['msg'] !== undefined) { _w.top.unrivalWorkInfo = ctResult['msg'] ; } } catch (e) { } busyThread += 1; GM_xmlhttpRequest({ method: "GET", headers: { 'Authorization': token, }, timeout: 6000, url: host + 'chaoXing/v3/getAnswer.php?tm=' + encodeURIComponent(quu['question'] .replace(/(^\s*)|(\s*$)/g, '')) + '&type=' + { '单选题': '0', '多选题': '1', '判断题': '3' }[quu['type']] + '&wid=' + wid + '&courseid=' + courseId, onload: function (res) { ctOnload(res, quu); }, onerror: function (err) { _w.top.unrivalWorkInfo = '查题错误,服务器连接失败(使用高峰期),等待一段时间'; console.log(err); busyThread -= 1; }, ontimeout: function (err) { _w.top.unrivalWorkInfo = '查题错误,服务器连接失败(使用高峰期),等待一段时间'; console.log(err); busyThread -= 1; } }); return; } try { var result = ctResult['data']; } catch (e) { _w.top.unrivalWorkInfo = '答案解析失败'; return; } _w.top.unrivalWorkInfo = '题目:' + quu['question'] + ':' + result; switch (quu['type']) { case '判断题': (function () { let answer = 'abaabaaba'; if ('正确是对√Tri'.indexOf(result) >= 0) { answer = 'true'; } else if ('错误否错×Fwr'.indexOf(result) >= 0) { answer = 'false'; } for (let u = 0, k = choiceEs.length; u < k; u++) { if (choiceEs[u].getAttribute('val-param') == answer && choiceEs[u].getAttribute( 'id-param') == quu['questionid'].replace( 'answer', '')) { choiceEs[u].click(); questionNum -= 1; return; } } if (randomDo == 1 && accuracy < 100) { _w.top.unrivalWorkInfo = quu['question'] + ':未找到正确答案,自动选【错】'; for (let u = 0, k = choiceEs.length; u < k; u++) { if (choiceEs[u].getElementsByTagName('em') .length < 1) { continue; } if (choiceEs[u].getAttribute('val-param') == 'false' && choiceEs[u].getAttribute( 'id-param') == quu['questionid'] .replace('answer', '')) { choiceEs[u].click(); return; } } } })(); break; case '单选题': (function () { let answerData = result; for (let option in quu['options']) { if (trim(option).replace(/\s/ig, '') == trim(answerData).replace(/\s/ig, '') || trim( option).replace(/\s/ig, '').includes(trim(answerData).replace(/\s/ig, '')) || trim(answerData).replace(/\s/ig, '').includes(trim(option).replace(/\s/ig, ''))) { for (let y = 0, j = choiceEs.length; y < j; y++) { if (choiceEs[y].getElementsByTagName( 'em').length < 1) { continue; } if (choiceEs[y].getElementsByTagName( 'em')[0].getAttribute( 'id-param') == quu['options'][ option ]['value'] && choiceEs[y] .getAttribute('id-param') == quu[ 'questionid'].replace('answer', '')) { if (!choiceEs[y].getAttribute( 'class').includes('cur')) { choiceEs[y].click(); } questionNum -= 1; return; } } } } if (randomDo == 1 && accuracy < 100) { _w.top.unrivalWorkInfo = quu['question'] + ':未找到正确答案,自动选【B】'; for (let y = 0, j = choiceEs.length; y < j; y++) { if (choiceEs[y].getElementsByTagName('em') .length < 1) { continue; } if (choiceEs[y].getElementsByTagName('em')[ 0].getAttribute('id-param') == 'B' && choiceEs[y].getAttribute( 'id-param') == quu['questionid'] .replace('answer', '')) { if (!choiceEs[y].getAttribute('class') .includes('cur')) { choiceEs[y].click(); } return; } } } })(); break; case '多选题': (function () { let answerData = trim(result).replace(/\s/ig, ''), hasAnswer = false; for (let option in quu['options']) { if (answerData.includes(trim(option).replace(/\s/ig, ''))) { for (let y = 0, j = choiceEs.length; y < j; y++) { if (choiceEs[y].getElementsByTagName( 'em').length < 1) { continue; } if (choiceEs[y].getElementsByTagName( 'em')[0].getAttribute( 'id-param') == quu['options'][ option ]['value'] && choiceEs[y] .getAttribute('id-param') == quu[ 'questionid'].replace('answer', '')) { if (!choiceEs[y].getAttribute( 'class').includes('cur')) { choiceEs[y].click(); } hasAnswer = true; break; } } } } if (hasAnswer) { questionNum -= 1; } else if (randomDo == 1 && accuracy < 100) { _w.top.unrivalWorkInfo = quu['question'] + ':未找到正确答案,自动全选'; for (let y = 0, j = choiceEs.length; y < j; y++) { if (choiceEs[y].getElementsByTagName('em') .length < 1) { continue; } if (choiceEs[y].getAttribute('id-param') == quu['questionid'].replace('answer', '') ) { if (!choiceEs[y].getAttribute('class') .includes('cur')) { choiceEs[y].click(); } } } } })(); break; } } catch (e) { console.log(e); } } for (let i = 0, l = questionList.length; i < l; i++) { nowTime += parseInt(Math.random() * 2000 + 2500, 10); setTimeout(function () { qu = questionList[i]; let param = 'question=' + encodeURIComponent( qu['question']); if (ctUrl.includes('icodef')) { param += '&type=' + { '单选题': '0', '多选题': '1', '判断题': '3' }[qu['type']] + '&id=' + wid; } GM_xmlhttpRequest({ method: "POST", headers: { 'Content-type': 'application/x-www-form-urlencoded', 'Authorization': token, }, url: ctUrl, timeout: 2000, data: param, onload: function (res) { ctOnload(res, qu); }, onerror: function () { ctOnload(false, qu); }, ontimeout: function () { ctOnload(false, qu); } }); }, nowTime); } var workInterval = setInterval(function () { if (busyThread != 0) { return; } clearInterval(workInterval); if (Math.floor((totalQuestionNum - questionNum) / totalQuestionNum) * 100 >= accuracy && _w.top .unrivalAutoSubmit == '1') { _w.top.unrivalDoneWorkId = getQueryVariable('workId'); _w.top.unrivalWorkInfo = '正确率符合标准,已提交答案'; setTimeout(function () { submitCheckTimes(); escapeBlank() submitAction() // setTimeout(function() { // document.querySelector(".cx_alert-blue").click() // }, parseInt(1000)); }, parseInt(Math.random() * 2000 + 3000, 10)); } else if (_w.top.unrivalAutoSave == 1) { _w.top.unrivalWorkInfo = '正确率不符合标准或未设置自动提交,已自动保存答案'; if (Math.floor((totalQuestionNum - questionNum) / totalQuestionNum) >= 0) { setTimeout(function () { _w.top.unrivalDoneWorkId = getQueryVariable('workId'); _w.noSubmit(); }, 2000); } } else { _w.top.unrivalWorkInfo = '用户设置为不自动保存答案,请手动提交或保存作业'; } }, 1000); } else if (_l.href.includes('work/phone/selectWorkQuestionYiPiYue')) { _w.top.unrivalWorkDone = true; _w.top.unrivalDoneWorkId = getQueryVariable('workId'); } else if (_l.href.includes('stat2-ans.chaoxing.com/task/s/index')) { if (_w.top == _w) { return; } _d.getElementsByClassName('page-container studentStatistic')[0].setAttribute('class', 'studentStatistic'); _d.getElementsByClassName('page-item item-task-list minHeight390')[0].remove(); _d.getElementsByClassName('subNav clearfix')[0].remove(); setInterval(function () { _l.reload(); }, 90000); } else if (_l.href.includes('passport2.') && _l.href.includes('login?refer=http') && autoLogin == 1) { if (!(/^1[3456789]\d{9}$/.test(phoneNumber))) { alert('自动登录的手机号填写错误,无法登陆') return; } if (password == '') { alert('未填写登录密码,无法登陆') return; } GM_xmlhttpRequest({ method: "get", url: 'https://passport2-api.chaoxing.com/v11/loginregister?cx_xxt_passport=json&uname=' + phoneNumber + '&code=' + encodeURIComponent(password), onload: function (res) { try { let ispass = JSON.parse(res.responseText); if (ispass['status']) { _l.href = decodeURIComponent(getQueryVariable('refer')); } else { alert(ispass['mes']); } } catch (err) { console.log(res.responseText); alert('登陆失败'); } }, onerror: function (err) { alert('登陆错误'); } }); } else if (_l.href.includes('unrivalxxtbackground')) { _d.getElementsByTagName("html")[0].innerHTML = ` 学习通挂机小助手

学习通挂机小助手 


任务列表
运行日志
`; var logs = { "logArry": [], "addLog": function (str, color = "black") { if (this.logArry.length >= 50) { this.logArry.splice(0, 1); } var nowTime = new Date(), nowHour = (Array(2).join(0) + nowTime.getHours()).slice(-2), nowMin = (Array(2).join(0) + nowTime.getMinutes()).slice(-2), nowSec = (Array(2).join(0) + nowTime.getSeconds()).slice(-2), logElement = _d.getElementById('log'), logStr = ""; this.logArry.push("[" + nowHour + ":" + nowMin + ":" + nowSec + "] " + str + ""); for (let logI = 0, logLen = this.logArry.length; logI < logLen; logI++) { logStr += this.logArry[logI] + "
"; } _d.getElementById('log').innerHTML = logStr; logElement.scrollTop = logElement.scrollHeight; } }; logs.addLog('此页面不必保持在最前端,后台会自动进行任务', 'green'); setInterval(function () { logs.addLog('此页面不必保持在最前端,后台会自动进行任务', 'green'); logs.addLog('如想禁用后台刷视频功能,请关闭脚本并重启浏览器', 'blue'); }, 120000) GM_addValueChangeListener('unrivalxxtbackgroundinfo', function (name, old_value, new_value, remote) { if (old_value != new_value) { logs.addLog(new_value); } }); setInterval(function () { if (Math.round(new Date() / 1000) - parseInt(GM_getValue('unrivalBackgroundVideoEnable', '6')) > 15) { logs.addLog('超星挂机小助手可能运行异常,如页面无反应,请尝试重启脚本或重启浏览器(脚本版本有此问题)'); } }, 10000); var loopShow = () => { let jobList = GM_getValue('unrivalBackgroundList', '1'); if (jobList == '1') { top.document.getElementById('joblist').innerHTML = '请将"超星挂机小助手"升级到最新版并重启浏览器'; } else { try { let jobHtml = ''; for (let i = 0, l = jobList.length; i < l; i++) { let status = ''; if (jobList[i]['done']) { status = '已完成'; } else if (parseInt(jobList[i]['playTime']) > 0) { status = '进行中'; } else { status = '等待中'; } if (jobList[i]['review']) { status += ':复习模式'; } jobHtml += `
` + '[' + status + ']' + jobList[i]['name'] + `
` } top.document.getElementById('joblist').innerHTML = jobHtml; } catch (e) { top.document.getElementById('joblist').innerHTML = '请将"超星挂机小助手"升级到最新版并重启浏览器!'; } } } loopShow(); setInterval(loopShow, 10000); } // ==================== 阅读助手菜单命令 ==================== // 添加菜单项 GM_registerMenuCommand("开始自动阅读", () => { if (pageOperator && pageOperator.isReadingPage()) { pageOperator.startAutoRead(); } else { readingNotify('请先导航到阅读页面', 'warning'); } }); GM_registerMenuCommand("显示阅读设置", () => { if (pageOperator) { pageOperator.showSettings(); } else { readingNotify('阅读助手未初始化', 'error'); } }); GM_registerMenuCommand("暂停阅读", () => { if (pageOperator && pageOperator.isReadingPage()) { pageOperator.pauseAutoRead(); } else { readingNotify('当前不在阅读页面', 'warning'); } }); GM_registerMenuCommand("测试自动跳转", () => { if (pageOperator && pageOperator.isCourseCatalogPage()) { pageOperator.handleCourseCatalogPage(); } else { readingNotify('当前不在课程目录页面', 'warning'); } }); // 初始化完成日志 console.log('脚本已加载 v' + GM_info.script.version); console.log('脚本已加载 v' + GM_info.script.version); })(); // ==================== 阅读助手配置和状态 ==================== // 全局配置 const READING_CONFIG = { scrollSpeed: parseFloat(GM_getValue('scrollSpeed', 0.5)), scrollMode: GM_getValue('scrollMode', 'paragraph'), autoStart: GM_getValue('autoStart', true), restartAfterFinish: GM_getValue('restartAfterFinish', true), showTips: GM_getValue('showTips', true), debugMode: GM_getValue('debugMode', true), // 新增:时长相关配置 taskDuration: GM_getValue('taskDuration', 0), // 任务时长(分钟) autoCalculateSpeed: GM_getValue('autoCalculateSpeed', true), // 是否自动计算速度 minSpeed: 0.1, // 最小速度 maxSpeed: 10, // 最大速度 // 新增:自动跳转章节配置 autoEnterChapter: GM_getValue('autoEnterChapter', true), // 是否自动进入章节 chapterEnterDelay: GM_getValue('chapterEnterDelay', 3000), // 进入章节延迟时间(ms) }; // 状态管理 const READING_STATE = { isRunning: false, isPaused: false, currentChapter: 0, totalChapters: 0, contentElements: [], currentElementIndex: 0, // 新增:时长相关状态 taskStartTime: null, estimatedTaskDuration: 0, // 预估任务时长(秒) contentLength: 0, // 内容长度(字符数) }; // 日志与通知 function readingLog(message, level = 'info') { if (!READING_CONFIG.debugMode && level !== 'error') return; console.log(`[超星阅读助手] [${level.toUpperCase()}] ${message}`); } function readingNotify(message, type = 'info') { // 只输出到控制台,不显示通知弹窗 readingLog(message, type); } // DOM工具 function waitForElement(selector, timeout = 15000) { return new Promise(resolve => { const interval = setInterval(() => { const el = document.querySelector(selector); if (el) { clearInterval(interval); resolve(el); } }, 100); setTimeout(() => clearInterval(interval), timeout); }); } // 页面操作类 class PageOperator { constructor() { this.init(); } init() { this.bindEvents(); this.setupGlobalKeyListener(); // 新增:设置全局键盘监听 this.detectPageType(); this.detectTaskDuration(); // 新增:检测任务时长 readingLog(`脚本初始化,版本:${GM_info.script.version}`); } // 新增:设置全局键盘监听器作为备用 setupGlobalKeyListener() { // 移除旧的全局监听器 if (this.globalKeyHandler) { document.removeEventListener('keydown', this.globalKeyHandler); } // 创建全局键盘处理器 this.globalKeyHandler = (e) => { // 只处理S键,确保设置面板始终可用 if (e.key.toLowerCase() === 's' && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') { e.preventDefault(); e.stopPropagation(); readingLog('全局S键被按下,显示设置面板'); this.showSettings(); } }; // 绑定全局监听器 document.addEventListener('keydown', this.globalKeyHandler, true); readingLog('全局键盘监听器已设置'); } // 新增:检测任务时长 detectTaskDuration() { try { // 尝试从页面中检测任务时长 const durationSelectors = [ '.task-duration', '.duration', '.time-limit', '[class*="duration"]', '[class*="time"]', '.task-info', '.course-info' ]; let detectedDuration = 0; for (const selector of durationSelectors) { const elements = document.querySelectorAll(selector); for (const el of elements) { const text = el.textContent; // 匹配各种时长格式:30分钟、30min、30分钟阅读等 const durationMatch = text.match(/(\d+)\s*(分钟|min|分钟阅读|分钟学习)/i); if (durationMatch) { detectedDuration = parseInt(durationMatch[1]); break; } } if (detectedDuration > 0) break; } if (detectedDuration > 0) { READING_CONFIG.taskDuration = detectedDuration; GM_setValue('taskDuration', detectedDuration); readingNotify(`检测到任务时长:${detectedDuration}分钟`); this.calculateOptimalSpeed(); } else { readingLog('未检测到任务时长,使用用户设置的时长'); } } catch (error) { readingLog(`检测任务时长失败: ${error.message}`, 'error'); } } // 新增:计算最优阅读速度 calculateOptimalSpeed() { if (!READING_CONFIG.taskDuration || !READING_CONFIG.autoCalculateSpeed) return; try { // 计算内容长度 this.collectContentElements(); READING_STATE.contentLength = READING_STATE.contentElements.reduce((total, el) => { return total + (el.textContent || '').length; }, 0); if (READING_STATE.contentLength === 0) { readingLog('无法计算内容长度,跳过速度计算'); return; } // 根据时长和内容长度计算速度 const targetDurationSeconds = READING_CONFIG.taskDuration * 60; const estimatedReadingTime = READING_STATE.contentLength / 500; // 假设每分钟阅读500字 const speedRatio = estimatedReadingTime / targetDurationSeconds; let optimalSpeed = READING_CONFIG.scrollSpeed * speedRatio; optimalSpeed = Math.max(READING_CONFIG.minSpeed, Math.min(READING_CONFIG.maxSpeed, optimalSpeed)); READING_CONFIG.scrollSpeed = parseFloat(optimalSpeed.toFixed(1)); GM_setValue('scrollSpeed', READING_CONFIG.scrollSpeed.toString()); readingNotify(`已根据任务时长(${READING_CONFIG.taskDuration}分钟)自动调整阅读速度为:${READING_CONFIG.scrollSpeed}秒/${READING_CONFIG.scrollMode === 'paragraph' ? '段落' : '页'}`); readingLog(`内容长度:${READING_STATE.contentLength}字符,预估阅读时间:${estimatedReadingTime.toFixed(1)}分钟,调整后速度:${READING_CONFIG.scrollSpeed}秒/单位`); } catch (error) { readingLog(`计算最优速度失败: ${error.message}`, 'error'); } } resetState() { READING_STATE.isRunning = false; READING_STATE.isPaused = false; READING_STATE.contentElements = []; READING_STATE.currentElementIndex = 0; readingLog('状态已重置'); } bindEvents() { // 移除旧的事件监听器 if (this.boundKeyDownHandler) { document.removeEventListener('keydown', this.boundKeyDownHandler); } // 绑定新的事件监听器 this.boundKeyDownHandler = this.handleKeyDown.bind(this); document.addEventListener('keydown', this.boundKeyDownHandler); } handleKeyDown(e) { // 防止在输入框中触发快捷键 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; // 确保S键在所有页面都能工作 if (e.key.toLowerCase() === 's') { e.preventDefault(); e.stopPropagation(); readingLog('S键被按下,显示设置面板'); this.showSettings(); return; } // 其他快捷键只在特定页面工作 switch(e.key.toLowerCase()) { case 'k': e.preventDefault(); if (this.isReadingPage()) { this.startAutoRead(); } break; case 'z': e.preventDefault(); if (this.isReadingPage()) { this.pauseAutoRead(); } break; case 'j': e.preventDefault(); // 新增:手动测试自动跳转功能 if (this.isCourseCatalogPage()) { readingLog('手动触发自动跳转测试'); this.handleCourseCatalogPage(); } else { readingNotify('当前页面不是课程目录页面', 'warning'); } break; } } detectPageType() { const oldTips = document.querySelector('#auto-read-tips'); if (oldTips) oldTips.remove(); if (this.isReadingPage()) { readingLog('检测到阅读页面'); this.initReading(); } else if (this.isCoursePage()) { readingLog('检测到课程主页'); } else if (location.href.includes('/mooc-ans/course/')) { readingLog('检测到课程目录/任务列表页面 (新)'); this.handleCourseCatalogPage(); } else if (this.isTaskPage()) { readingLog('检测到任务页面 (旧)'); } else if (this.isCourseCatalogPage()) { readingLog('检测到课程目录页面'); this.handleCourseCatalogPage(); } } isReadingPage() { return location.href.includes('/ztnodedetailcontroller/visitnodedetail'); } isCoursePage() { return location.href.includes('/mooc-ans/mycourse/studentstudy'); } isTaskPage() { return location.href.includes('pageHeader=0'); } isCourseCatalogPage() { // 检测课程目录页面的URL模式 const pathname = location.pathname; const href = location.href; // 更宽松的URL匹配规则 const urlPatterns = [ /\/mooc2-ans\/course\/\d+\.html$/, /\/mooc2-ans\/zt\/\d+\.html$/, /\/mooc-ans\/course\/\d+\.html$/, /\/mooc-ans\/zt\/\d+\.html$/, /\/course\/\d+\.html$/, /\/zt\/\d+\.html$/, /\/mooc-ans\/course\/\d+$/, /\/mooc-ans\/zt\/\d+$/, /\/course\/\d+$/, /\/zt\/\d+$/ ]; // 检查URL模式 const urlMatch = urlPatterns.some(pattern => pattern.test(pathname)); // 检查页面内容特征 const hasCourseSection = document.querySelector('.course_section') !== null; const hasChapterText = document.querySelector('.chapterText') !== null; const hasCatalogItems = document.querySelector('.catalog-item') !== null; readingLog(`页面检测: URL=${pathname}, URL匹配=${urlMatch}, 有课程章节=${hasCourseSection}, 有章节文本=${hasChapterText}, 有目录项=${hasCatalogItems}`); return urlMatch && (hasCourseSection || hasChapterText || hasCatalogItems); } // 处理课程目录页面 - 自动进入第一章节 handleCourseCatalogPage() { if (!READING_CONFIG.autoEnterChapter) { readingLog('自动进入章节功能已禁用'); return; } try { readingLog('准备自动进入第一章节'); readingNotify('检测到课程目录页面,即将自动进入第一章节...'); setTimeout(() => { // 尝试多种选择器来找到第一章节 const selectors = [ '.course_section:first-child .chapterText', '.course_section .chapterText:first-child', '.course_section .chapterText', '.catalog-item:first-child', '.catalog-item', '.chapter-item:first-child', '.chapter-item', 'a[href*="/ztnodedetailcontroller/visitnodedetail"]:first-child', 'a[href*="/ztnodedetailcontroller/visitnodedetail"]' ]; let firstChapter = null; for (const selector of selectors) { firstChapter = document.querySelector(selector); if (firstChapter) { readingLog(`使用选择器找到章节: ${selector}`); break; } } if (firstChapter) { readingLog('找到第一章节,正在点击进入'); // 高亮显示即将点击的元素 firstChapter.style.outline = '3px solid red'; firstChapter.style.transition = 'outline 0.3s ease'; // 滚动到元素位置 firstChapter.scrollIntoView({ behavior: 'smooth', block: 'center' }); // 点击元素 firstChapter.click(); readingNotify('已自动进入第一章节'); // 移除高亮 setTimeout(() => { firstChapter.style.outline = ''; }, 2000); } else { readingLog('未找到第一章节按钮', 'warning'); readingNotify('未找到第一章节,请手动点击', 'warning'); // 输出页面结构信息用于调试 const courseSections = document.querySelectorAll('.course_section'); const chapterTexts = document.querySelectorAll('.chapterText'); const catalogItems = document.querySelectorAll('.catalog-item'); readingLog(`调试信息: course_section数量=${courseSections.length}, chapterText数量=${chapterTexts.length}, catalog-item数量=${catalogItems.length}`); if (courseSections.length > 0) { readingLog(`第一个course_section内容: ${courseSections[0].innerHTML.substring(0, 200)}...`); } } }, READING_CONFIG.chapterEnterDelay); } catch (error) { readingLog(`自动进入章节失败: ${error.message}`, 'error'); readingNotify('自动进入章节失败', 'error'); } } initReading() { this.showUsageTips(); this.detectChapterInfo(); this.collectContentElements(); if (READING_CONFIG.autoStart) { setTimeout(() => { if (this.isReadingPage()) { this.startAutoRead(); } }, 2000); } } startAutoRead() { if (READING_STATE.isRunning && !READING_STATE.isPaused) return; READING_STATE.isRunning = true; READING_STATE.isPaused = false; if (READING_CONFIG.scrollMode === 'paragraph') { this.startParagraphScroll(); } else { this.startPageScroll(); } this.showStatus(); readingNotify(`开始阅读 (${READING_CONFIG.scrollSpeed}秒/${READING_CONFIG.scrollMode === 'paragraph' ? '段落' : '页'})`); } pauseAutoRead() { if (!READING_STATE.isRunning) return; READING_STATE.isPaused = true; clearTimeout(this.scrollTimer); this.showStatus(); readingNotify('阅读已暂停'); } showStatus() { const status = READING_STATE.isPaused ? '已暂停' : '阅读中'; const progress = READING_STATE.totalChapters > 0 ? `第 ${READING_STATE.currentChapter}/${READING_STATE.totalChapters} 章` : '章节信息未知'; this.showModal(`

超星阅读助手

状态: ${status}

${progress}

模式: ${READING_CONFIG.scrollMode === 'paragraph' ? '段落阅读' : '页面阅读'}

速度: ${READING_CONFIG.scrollSpeed.toFixed(1)}秒/${READING_CONFIG.scrollMode === 'paragraph' ? '段落' : '页'}

按 Z 键暂停 / 按 K 键继续

`); setTimeout(() => this.hideModal(), 3000); } startParagraphScroll() { this.collectContentElements(); if (READING_STATE.contentElements.length === 0) { readingNotify('未检测到段落,临时切换整页模式...', 'warning'); READING_CONFIG.scrollMode = 'page'; this.startPageScroll(); // 启动内容监听器 const observer = new MutationObserver(() => { this.collectContentElements(); if (READING_STATE.contentElements.length > 0) { observer.disconnect(); readingNotify('检测到段落内容,恢复逐段模式', 'success'); READING_CONFIG.scrollMode = 'paragraph'; if (!READING_STATE.isPaused) { this.startParagraphScroll(); } } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true }); return; } this.scrollToNextElement(); } collectContentElements() { const selectors = [ 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img', 'video', 'iframe', '.content', '.text-block', '.article-content' ]; READING_STATE.contentElements = []; selectors.forEach(selector => { const elements = document.querySelectorAll(selector); elements.forEach(el => { if (el.offsetHeight > 20 && el.offsetWidth > 20) { READING_STATE.contentElements.push(el); } }); }); READING_STATE.contentElements.sort((a, b) => { return a.getBoundingClientRect().top - b.getBoundingClientRect().top; }); readingLog(`找到 ${READING_STATE.contentElements.length} 个内容元素`); } scrollToNextElement() { if (READING_STATE.isPaused) return; if (READING_STATE.currentElementIndex >= READING_STATE.contentElements.length) { this.onChapterComplete(); return; } const element = READING_STATE.contentElements[READING_STATE.currentElementIndex]; if (READING_CONFIG.debugMode) { element.style.outline = '2px solid red'; setTimeout(() => element.style.outline = '', 1000); } element.scrollIntoView({ behavior: 'smooth', block: 'center' }); const baseTime = parseFloat(READING_CONFIG.scrollSpeed) * 1000; let waitTime = element.tagName === 'IMG' || element.tagName === 'VIDEO' ? baseTime * 1.5 : baseTime; READING_STATE.currentElementIndex++; this.scrollTimer = setTimeout(() => this.scrollToNextElement(), waitTime); } startPageScroll() { const scrollSpeed = parseFloat(READING_CONFIG.scrollSpeed); const totalHeight = document.documentElement.scrollHeight - window.innerHeight; const scrollStep = totalHeight / (scrollSpeed * 10); let currentTop = 0; clearInterval(this.scrollTimer); this.scrollTimer = setInterval(() => { if (READING_STATE.isPaused) return; currentTop += scrollStep; if (currentTop >= totalHeight) { clearInterval(this.scrollTimer); this.onChapterComplete(); } else { window.scrollTo({ top: currentTop, behavior: 'smooth' }); } }, 100); } onChapterComplete() { clearTimeout(this.scrollTimer); READING_STATE.currentElementIndex = 0; READING_STATE.contentElements = []; this.findNextButton() .then(nextButton => { if (nextButton) { readingNotify('正在加载下一章...'); nextButton.style.outline = '3px solid green'; setTimeout(() => nextButton.style.outline = '', 2000); nextButton.click(); READING_STATE.currentChapter++; } else { if (READING_CONFIG.restartAfterFinish) { readingNotify('已到达最后一章,即将从头开始', 'warning'); setTimeout(() => this.goToFirstChapter(), 3000); } else { READING_STATE.isRunning = false; readingNotify('全部阅读完成!', 'success'); } } }) .catch(error => { readingNotify(`章节切换错误: ${error.message}`, 'error'); readingLog(error.stack, 'error'); }); } findNextButton() { return new Promise(resolve => { const selectors = [ READING_CONFIG.nextButtonSelector, '.nodeItem.r i', '.next-page-btn', 'a:contains("下一章")', 'button:contains("下一章")', 'a[title="下一章"]', '.reader__control--next', '.uxp-pager-next', 'button[aria-label*="下一章"]', '.next-btn:visible' ]; for (const selector of selectors) { try { let element = document.querySelector(selector); if (!element && selector.includes(':contains(')) { const text = selector.match(/:contains\("(.*)"\)/)[1]; const allElements = document.querySelectorAll('a, button'); element = Array.from(allElements).find(el => el.textContent.includes(text)); } if (element) { readingLog(`找到下一章按钮: ${selector}`); return resolve(element); } } catch (error) { // 继续尝试下一个选择器 } } readingLog('未找到下一章按钮,尝试通用选择器', 'warning'); const genericElements = document.querySelectorAll('*'); for (const el of genericElements) { if (el.textContent.includes('下一章') && el.offsetWidth > 0 && el.offsetHeight > 0) { readingLog('找到下一章按钮(通用选择器)'); return resolve(el); } } readingLog('未找到下一章按钮', 'warning'); resolve(null); }); } goToFirstChapter() { try { const firstChapter = document.querySelector('.course_section .chapterText, .catalog-item:first-child'); if (firstChapter) { firstChapter.click(); setTimeout(() => { READING_STATE.currentChapter = 1; if (READING_CONFIG.scrollMode === 'paragraph') { this.collectContentElements(); } if (!READING_STATE.isPaused) { this.scrollToNextElement(); } }, 3000); } else { readingNotify('未找到目录,无法重新开始', 'error'); READING_STATE.isRunning = false; } } catch (error) { readingNotify(`跳转错误: ${error.message}`, 'error'); READING_STATE.isRunning = false; } } // 设置菜单 showSettings() { const html = `

超星阅读助手设置

生活不易,猪猪叹气 —— 赏口饲料,让我少气!🐷✨

输入任务要求的时长,脚本会自动计算最优阅读速度
根据任务时长自动调整阅读速度
支持 0.1 - 30 秒(如:0.5 秒/页,1.2 秒/段落)
在课程目录页面自动点击第一章节
单位: 毫秒,延迟时间越长越稳定
`; this.showModal(html); document.getElementById('save-settings').addEventListener('click', () => { READING_CONFIG.taskDuration = parseInt(document.getElementById('task-duration').value); READING_CONFIG.autoCalculateSpeed = document.getElementById('auto-calculate-speed').checked; READING_CONFIG.scrollSpeed = parseFloat(document.getElementById('scroll-speed').value).toFixed(1); READING_CONFIG.scrollMode = document.getElementById('scroll-mode').value; READING_CONFIG.autoStart = document.getElementById('auto-start').checked; READING_CONFIG.restartAfterFinish = document.getElementById('restart-after-finish').checked; READING_CONFIG.showTips = document.getElementById('show-tips').checked; READING_CONFIG.debugMode = document.getElementById('debug-mode').checked; READING_CONFIG.autoEnterChapter = document.getElementById('auto-enter-chapter').checked; READING_CONFIG.chapterEnterDelay = parseInt(document.getElementById('chapter-enter-delay').value); GM_setValue('taskDuration', READING_CONFIG.taskDuration); GM_setValue('autoCalculateSpeed', READING_CONFIG.autoCalculateSpeed); GM_setValue('scrollSpeed', READING_CONFIG.scrollSpeed.toString()); GM_setValue('scrollMode', READING_CONFIG.scrollMode); GM_setValue('autoStart', READING_CONFIG.autoStart); GM_setValue('restartAfterFinish', READING_CONFIG.restartAfterFinish); GM_setValue('showTips', READING_CONFIG.showTips); GM_setValue('debugMode', READING_CONFIG.debugMode); GM_setValue('autoEnterChapter', READING_CONFIG.autoEnterChapter); GM_setValue('chapterEnterDelay', READING_CONFIG.chapterEnterDelay); // 如果启用了自动计算速度,重新计算 if (READING_CONFIG.autoCalculateSpeed && READING_CONFIG.taskDuration > 0) { this.calculateOptimalSpeed(); } readingNotify('设置已保存'); this.hideModal(); }); document.getElementById('close-settings').addEventListener('click', () => { this.hideModal(); }); // 添加测试按钮事件监听器 document.getElementById('test-page-detection').addEventListener('click', () => { const isCatalog = this.isCourseCatalogPage(); const isReading = this.isReadingPage(); const isCourse = this.isCoursePage(); const isTask = this.isTaskPage(); const result = `

页面检测结果:

当前URL: ${location.href}

当前路径: ${location.pathname}

是课程目录页: ${isCatalog ? '是' : '否'}

是阅读页面: ${isReading ? '是' : '否'}

是课程主页: ${isCourse ? '是' : '否'}

是任务页面: ${isTask ? '是' : '否'}

自动进入章节: ${READING_CONFIG.autoEnterChapter ? '已开启' : '已关闭'}

`; this.showModal(result); }); document.getElementById('test-auto-jump').addEventListener('click', () => { if (this.isCourseCatalogPage()) { readingLog('手动触发自动跳转测试'); this.handleCourseCatalogPage(); } else { readingNotify('当前页面不是课程目录页面', 'warning'); } }); document.getElementById('test-s-key').addEventListener('click', () => { readingLog('手动测试S键功能'); readingNotify('正在测试S键功能...'); // 模拟S键按下 const event = new KeyboardEvent('keydown', { key: 's', code: 'KeyS', keyCode: 83, which: 83, bubbles: true, cancelable: true }); document.dispatchEvent(event); setTimeout(() => { readingNotify('S键测试完成,如果设置面板没有出现,请检查控制台日志'); }, 100); }); } // 弹窗相关方法 createModal() { if (document.getElementById('auto-read-modal')) return; const modal = document.createElement('div'); modal.id = 'auto-read-modal'; modal.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.2); z-index: 9999; min-width: 300px; max-width: 80%; display: none; font-family: Arial, sans-serif; `; document.body.appendChild(modal); } showModal(content) { this.createModal(); const modal = document.getElementById('auto-read-modal'); modal.innerHTML = content; modal.style.display = 'block'; } hideModal() { const modal = document.getElementById('auto-read-modal'); if (modal) modal.style.display = 'none'; } // 显示操作提示 showUsageTips() { if (!READING_CONFIG.showTips) return; if (!location.href.includes("/ztnodedetailcontroller/visitnodedetail")) return; const oldTips = document.querySelector('#auto-read-tips'); if (oldTips) oldTips.remove(); const tips = `

超星阅读助手快捷键:

• K: 开始/继续阅读

• Z: 暂停阅读

• S: 显示设置

• J: 测试自动跳转 (目录页)

• 任务时长: ${READING_CONFIG.taskDuration > 0 ? READING_CONFIG.taskDuration + '分钟' : '未设置'}

• 自动进入章节: ${READING_CONFIG.autoEnterChapter ? '已开启' : '已关闭'}

`; document.body.insertAdjacentHTML('beforeend', tips); } // 添加快速启动按钮 addQuickStartButton() { try { const courseTitle = document.querySelector('.course-title') || document.querySelector('.course-name') || document.querySelector('h1'); if (courseTitle) { const startButton = document.createElement('button'); startButton.textContent = '🚀 开始自动阅读'; startButton.style.cssText = ` background-color: #4CAF50; color: white; border: none; padding: 8px 16px; margin-left: 10px; border-radius: 4px; cursor: pointer; font-size: 14px; `; startButton.addEventListener('click', () => { const firstChapter = document.querySelector('.course_section .chapterText, .catalog-item:first-child'); if (firstChapter) { firstChapter.click(); setTimeout(() => { this.detectChapterInfo(); this.startAutoRead(); }, 3000); } else { readingNotify('未找到章节列表', 'error'); } }); courseTitle.parentNode.insertBefore(startButton, courseTitle.nextSibling); } } catch (error) { readingLog(`添加快速启动按钮失败: ${error.message}`, 'error'); } } // 检测章节信息 detectChapterInfo() { try { const chapterTitle = document.querySelector('.node-title, .chapter-title, h1')?.textContent || '未知章节'; const chapterElements = document.querySelectorAll('.course_section .chapterText, .catalog-item'); const currentChapterElement = document.querySelector('.course_section .chapterText.active, .catalog-item.active'); if (chapterElements.length > 0) { READING_STATE.totalChapters = chapterElements.length; if (currentChapterElement) { const chapterArray = Array.from(chapterElements); READING_STATE.currentChapter = chapterArray.indexOf(currentChapterElement) + 1; readingLog(`当前章节: ${READING_STATE.currentChapter}/${READING_STATE.totalChapters} - ${chapterTitle}`); } else { readingLog(`总章节数: ${READING_STATE.totalChapters} - 无法确定当前章节`); } } else { readingLog('无法获取章节信息', 'warning'); } } catch (error) { readingLog(`检测章节信息失败: ${error.message}`, 'error'); } } } // 初始化页面操作类 let pageOperator = null; try { console.log('开始初始化PageOperator...'); pageOperator = new PageOperator(); console.log('PageOperator初始化成功'); } catch (error) { console.error('初始化阅读助手失败:', error); console.error(`PageOperator初始化失败: ${error.message}`); console.error(`错误堆栈: ${error.stack}`); } // 强制检测当前页面类型(用于调试) setTimeout(() => { try { console.log('=== 开始强制页面检测 ==='); console.log(`当前URL: ${location.href}`); console.log(`当前路径: ${location.pathname}`); console.log(`PageOperator是否存在: ${pageOperator ? '是' : '否'}`); if (pageOperator) { console.log(`是课程目录页: ${pageOperator.isCourseCatalogPage()}`); console.log(`是阅读页面: ${pageOperator.isReadingPage()}`); console.log(`是课程主页: ${pageOperator.isCoursePage()}`); console.log(`是任务页面: ${pageOperator.isTaskPage()}`); console.log(`自动进入章节: ${READING_CONFIG.autoEnterChapter ? '已开启' : '已关闭'}`); // 如果是课程目录页面,强制触发自动跳转 if (pageOperator.isCourseCatalogPage() && READING_CONFIG.autoEnterChapter) { console.log('检测到课程目录页面,强制触发自动跳转'); pageOperator.handleCourseCatalogPage(); } } else { console.log('PageOperator未初始化,尝试重新初始化...'); try { pageOperator = new PageOperator(); console.log('PageOperator重新初始化成功'); } catch (error) { console.error(`PageOperator重新初始化失败: ${error.message}`); } } } catch (error) { console.error('强制页面检测失败:', error); } }, 1000); // 添加简单的测试功能 setTimeout(() => { try { console.log('=== 阅读助手功能测试 ==='); console.log(`READING_CONFIG存在: ${typeof READING_CONFIG !== 'undefined'}`); console.log(`READING_STATE存在: ${typeof READING_STATE !== 'undefined'}`); console.log(`PageOperator类存在: ${typeof PageOperator !== 'undefined'}`); console.log(`pageOperator实例存在: ${pageOperator ? '是' : '否'}`); // 测试键盘事件 document.addEventListener('keydown', (e) => { if (e.key.toLowerCase() === 's' && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') { console.log('S键被按下,测试阅读助手功能'); if (pageOperator) { pageOperator.showSettings(); } else { console.log('PageOperator未初始化,无法显示设置'); } } }); console.log('阅读助手功能测试完成,按S键测试设置面板'); } catch (error) { console.error('阅读助手功能测试失败:', error); } }, 2000);