// ==UserScript== // @name 智学 - 学习通自动答题+倍速 // @namespace zhixue-assistant // @version 3.5.3 // @description 全自动刷课:倍速播放+自动下一节+AI答题+字体解密+AI总结+红金共青团风格v5 // @icon https://s.coze.cn/image/Cc2jwhdqgjU/ // @author zhixue // @match *://*.chaoxing.com/* // @match *://*.edu.cn/* // @match *://*.nbdlib.cn/* // @match *://*.hnsyu.net/* // @match *://*.ac.cn/* // @match *://*.mooc1.cn/* // @match *://*.mooc1-1.cn/* // @match *://*.mooc1-2.cn/* // @match *://*/* // @include * // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_notification // @grant GM_download // @grant GM_openInTab // @connect api.zhixue.icu // @connect forestpolice.org // @connect chaoxing.com // @connect static.chaoxing.com // @connect scriptcat.org // @connect pan-yz.chaoxing.com // @connect mooc1.chaoxing.com // @connect d0.ananas.chaoxing.com // @connect contestyd.chaoxing.com // @connect pan-yz.chaoxing.com // @run-at document-idle // @license MIT // ==/UserScript== (function() { 'use strict'; /** * Combined by jsDelivr. * Original files: * - /gh/photopea/Typr.js@15aa12ffa6cf39e8788562ea4af65b42317375fb/src/Typr.min.js * - /gh/photopea/Typr.js@f4fcdeb8014edc75ab7296bd85ac9cde8cb30489/src/Typr.U.min.js * - /npm/blueimp-md5@2.19.0/js/md5.min.js * * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files */ /** * Minified by jsDelivr using Terser v5.10.0. * Original file: /gh/photopea/Typr.js@15aa12ffa6cf39e8788562ea4af65b42317375fb/src/Typr.js * * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files */ var Typr={parse:function(r){var e=function(r,e,a,t){Typr.B;var n=Typr.T,o={cmap:n.cmap,head:n.head,hhea:n.hhea,maxp:n.maxp,hmtx:n.hmtx,name:n.name,"OS/2":n.OS2,post:n.post,loca:n.loca,kern:n.kern,glyf:n.glyf,"CFF ":n.CFF,"SVG ":n.SVG},i={_data:r,_index:e,_offset:a};for(var s in o){var d=Typr.findTable(r,s,a);if(d){var u=d[0],h=t[u];null==h&&(h=o[s].parseTab(r,u,d[1],i)),i[s]=t[u]=h}}return i},a=Typr.B,t=new Uint8Array(r),n={};if("ttcf"==a.readASCII(t,0,4)){var o=4;a.readUshort(t,o);o+=2;a.readUshort(t,o);o+=2;var i=a.readUint(t,o);o+=4;for(var s=[],d=0;d>8&255,r[e+1]=255&a},readUshorts:function(r,e,a){for(var t=[],n=0;n>24&255,r[e+1]=a>>16&255,r[e+2]=a>>8&255,r[e+3]=a>>0&255},readUint64:function(r,e){return 4294967296*Typr.B.readUint(r,e)+Typr.B.readUint(r,e+4)},readASCII:function(r,e,a){for(var t="",n=0;n255?-1:Typr.T.CFF.glyphByUnicode(r,Typr.T.CFF.tableSE[e])},readCharset:function(r,e,a){var t=Typr.B,n=[".notdef"],o=r[e];if(e++,0==o)for(var i=0;i>4,v=15&f;if(15!=l&&p.push(l),15!=v&&p.push(v),15==v)break}for(var y="",c=[0,1,2,3,4,5,6,7,8,9,".","e","e-","reserved","-","endOfNumber"],S=0;S>>1;a.searchRange=n(r,e),e+=2,a.entrySelector=n(r,e),e+=2,a.rangeShift=n(r,e),e+=2,a.endCount=o(r,e,u),e+=2*u,e+=2,a.startCount=o(r,e,u),e+=2*u,a.idDelta=[];for(var h=0;h>>1),a},parse6:function(r,e,a){var t=Typr.B;e+=2;t.readUshort(r,e);e+=2;t.readUshort(r,e);e+=2,a.firstCode=t.readUshort(r,e),e+=2;var n=t.readUshort(r,e);e+=2,a.glyphIdArray=[];for(var o=0;o=i.xMax||i.yMin>=i.yMax)return null;if(i.noc>0){i.endPts=[];for(var s=0;s>>8;0==(h&=15)&&(e=o.readFormat0(r,e,s))}return s},parseV1:function(r,e,a,t){var n=Typr.B,o=Typr.T.kern,i=(n.readFixed(r,e),n.readUint(r,e+4));e+=8;for(var s={glyph1:[],rval:[]},d=0;d65535&&a++,n.push(Typr.U.codeToGlyph(t,h))}var o=[];for(a=0;a>>1);t[a*e]<=r?s=a:n=a}return s*e},o=r.tables[s],f=o.format,i=-1;if(0==f)i=e>=o.map.length?0:o.map[e];else if(4==f){var l=-1,c=o.endCount;if(e>c[c.length-1]?l=-1:c[l=h(c,1,e)]>1)-(o.idRangeOffset.length-l)]:e+o.idDelta[l])}}else if(6==f){var u=e-o.firstCode,d=o.glyphIdArray;i=u<0||u>=d.length?0:d[u]}else{if(12!=f)throw"unknown cmap table format "+o.format;var v=o.groups;e>v[v.length-2]?i=0:(v[a=h(v,3,e)]<=e&&e<=v[a+1]&&(i=v[a+2]+(e-v[a])),-1==i&&(i=0))}var p=t["SVG "],g=t.loca;return 0==i||null!=t["CFF "]||null!=p&&null!=p.entries[i]||g[i]!=g[i+1]||-1!=[9,10,11,12,13,32,133,160,5760,8232,8233,8239,12288,6158,8203,8204,8205,8288,65279].indexOf(e)||8192<=e&&e<=8202||(i=0),i},glyphToPath:function(t,e){var r={cmds:[],crds:[]},s=t["SVG "],n=t["CFF "],a=Typr.U;if(s&&s.entries[e]){var h=s.entries[e];null!=h&&("string"==typeof h&&(h=a.SVG.toPath(h),s.entries[e]=h),r=h)}else if(n){var o=n.Private,f={x:0,y:0,stack:[],nStems:0,haveWidth:!1,width:o?o.defaultWidthX:0,open:!1};if(n.ROS){for(var i=0;n.FDSelect[i+2]<=e;)i+=2;o=n.FDArray[n.FDSelect[i+1]].Private}a._drawCFF(n.CharStrings[e],f,n,o,r)}else t.glyf&&a._drawGlyf(e,t,r);return{cmds:r.cmds,crds:r.crds}},_drawGlyf:function(t,e,r){var s=e.glyf[t];null==s&&(s=e.glyf[t]=Typr.T.glyf._parseGlyf(e,t)),null!=s&&(s.noc>-1?Typr.U._simpleGlyph(s,r):Typr.U._compoGlyph(s,e,r))},_simpleGlyph:function(t,e){for(var r=Typr.U.P,s=0;s>1,a.length=0,o=!0;else if("o3"==S||"o23"==S){a.length%2!=0&&!o&&(f=a.shift()+P),h+=a.length>>1,a.length=0,o=!0}else if("o4"==S)a.length>1&&!o&&(f=a.shift()+P,o=!0),i&&x.ClosePath(n),u+=a.pop(),x.MoveTo(n,c,u),i=!0;else if("o5"==S)for(;a.length>0;)c+=a.shift(),u+=a.shift(),x.LineTo(n,c,u);else if("o6"==S||"o7"==S)for(var F=a.length,A="o6"==S,U=0;UMath.abs(C-u)?c=T+a.shift():u=C+a.shift(),x.CurveTo(n,d,v,p,g,b,_),x.CurveTo(n,m,y,T,C,c,u));else if("o14"==S){if(a.length>0&&!o&&(f=a.shift()+r.nominalWidthX,o=!0),4==a.length){var k=a.shift(),O=a.shift(),V=a.shift(),W=a.shift(),B=M.glyphBySE(r,V),I=M.glyphBySE(r,W);Typr.U._drawCFF(r.CharStrings[B],e,r,s,n),e.x=k,e.y=O,Typr.U._drawCFF(r.CharStrings[I],e,r,s,n)}i&&(x.ClosePath(n),i=!1)}else if("o19"==S||"o20"==S){a.length%2!=0&&!o&&(f=a.shift()+P),h+=a.length>>1,a.length=0,o=!0,l+=h+7>>3}else if("o21"==S)a.length>2&&!o&&(f=a.shift()+P,o=!0),u+=a.pop(),c+=a.pop(),i&&x.ClosePath(n),x.MoveTo(n,c,u),i=!0;else if("o22"==S)a.length>1&&!o&&(f=a.shift()+P,o=!0),c+=a.pop(),i&&x.ClosePath(n),x.MoveTo(n,c,u),i=!0;else if("o25"==S){for(;a.length>6;)c+=a.shift(),u+=a.shift(),x.LineTo(n,c,u);d=c+a.shift(),v=u+a.shift(),p=d+a.shift(),g=v+a.shift(),c=p+a.shift(),u=g+a.shift(),x.CurveTo(n,d,v,p,g,c,u)}else if("o26"==S)for(a.length%2&&(c+=a.shift());a.length>0;)d=c,v=u+a.shift(),c=p=d+a.shift(),u=(g=v+a.shift())+a.shift(),x.CurveTo(n,d,v,p,g,c,u);else if("o27"==S)for(a.length%2&&(u+=a.shift());a.length>0;)v=u,p=(d=c+a.shift())+a.shift(),g=v+a.shift(),c=p+a.shift(),u=g,x.CurveTo(n,d,v,p,g,c,u);else if("o10"==S||"o29"==S){var q="o10"==S?s:r;if(0==a.length)console.log("error: empty stack");else{var Q=a.pop(),X=q.Subrs[Q+q.Bias];e.x=c,e.y=u,e.nStems=h,e.haveWidth=o,e.width=f,e.open=i,Typr.U._drawCFF(X,e,r,s,n),c=e.x,u=e.y,h=e.nStems,o=e.haveWidth,f=e.width,i=e.open}}else if("o30"==S||"o31"==S){var D=a.length,E=(L=0,"o31"==S);for(L+=D-(F=-3&D);L0&&"e"!=r[a-1]&&(r=r.slice(0,a)+" "+r.slice(a),a++,n=!0)}if(r=r.split(/\s*[\s,]\s*/).map(parseFloat),"translate"==e)1==r.length?t.translate(s,r[0],0):t.translate(s,r[0],r[1]);else if("scale"==e)1==r.length?t.scale(s,r[0],r[0]):t.scale(s,r[0],r[1]);else if("rotate"==e){var o=0,f=0;1!=r.length&&(o=r[1],f=r[2]),t.translate(s,-o,-f),t.rotate(s,-Math.PI*r[0]/180),t.translate(s,o,f)}else"matrix"==e?s=r:console.log("unknown transform: ",e);return s}function n(e,s,a){for(var o=0;o=0?1:-1)*Math.acos(Math.max(-1,Math.min(1,n)))},N=(V-E)/w,j=(W-H)/S,J=z(1,0,N,j),K=z(N,j,(-V-E)/w,(-W-H)/S);!function(t,e,r,s,n,a,h){var o=function(t,e){var r=Math.sin(e),s=Math.cos(e),n=(e=t[0],t[1]),a=t[2],h=t[3];t[0]=e*s+n*r,t[1]=-e*r+n*s,t[2]=a*s+h*r,t[3]=-a*r+h*s},f=function(t,e){for(var r=0;rn;)a-=2*Math.PI;else for(;a>>2,a=n.hb_buffer_get_glyph_positions(t,0)>>>2,h=0;h>16)+(t>>16)+(r>>16)<<16|65535&r}function f(n,t,r,e,o,u){return d((u=d(d(t,n),d(e,u)))<>>32-o,r)}function l(n,t,r,e,o,u,c){return f(t&r|~t&e,n,t,o,u,c)}function g(n,t,r,e,o,u,c){return f(t&e|r&~e,n,t,o,u,c)}function v(n,t,r,e,o,u,c){return f(t^r^e,n,t,o,u,c)}function m(n,t,r,e,o,u,c){return f(r^(t|~e),n,t,o,u,c)}function c(n,t){var r,e,o,u;n[t>>5]|=128<>>9<<4)]=t;for(var c=1732584193,f=-271733879,i=-1732584194,a=271733878,h=0;h>5]>>>e%32&255);return t}function a(n){var t=[];for(t[(n.length>>2)-1]=void 0,e=0;e>5]|=(255&n.charCodeAt(e/8))<>>4&15)+r.charAt(15&t);return e}function r(n){return unescape(encodeURIComponent(n))}function o(n){return i(c(a(n=r(n)),8*n.length))}function u(n,t){return function(n,t){var r,e=a(n),o=[],u=[];for(o[15]=u[15]=void 0,16 { const t = setTimeout(r, ms); const c = setInterval(() => { if (!Answer.running) { clearTimeout(t); clearInterval(c); r(); } }, 200); }); } function isTop() { return window.self === window.top; } // ========================================== // API 层(替代 chrome.runtime.sendMessage + background.js) // ========================================== const Api = { _tokenKey: 'zhixue_authToken', _userKey: 'zhixue_userInfo', getToken() { return GM_getValue(this._tokenKey, ''); }, setToken(token) { GM_setValue(this._tokenKey, token); }, getUserInfo() { try { return JSON.parse(GM_getValue(this._userKey, '{}')); } catch { return {}; } }, setUserInfo(data) { GM_setValue(this._userKey, JSON.stringify({ username: data.username, quota: data.quota, used: data.used, isWhitelisted: data.is_whitelisted, })); }, clearAuth() { GM_deleteValue(this._tokenKey); GM_deleteValue(this._userKey); }, // 答题 API(替代 background.js 的 SEARCH_ANSWER) async searchAnswer(question, rawQuestion, questionType, options, forceAi = false) { const token = this.getToken(); if (!token) { return { success: false, error: '未登录,请先登录', code: 'AUTH_REQUIRED' }; } return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'POST', url: `${API_BASE}/api/answer`, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, data: JSON.stringify({ question, rawQuestion, questionType, options, forceAi }), timeout: 60000, onload: (resp) => { try { const data = JSON.parse(resp.responseText); if (resp.status === 401) { this.clearAuth(); resolve({ success: false, error: '登录已过期,请重新登录', code: 'AUTH_EXPIRED' }); return; } if (resp.status === 403 && data.code === 'QUOTA_EXCEEDED') { resolve({ success: false, error: '额度不足,请充值', code: 'QUOTA_EXCEEDED' }); return; } resolve(data); } catch (e) { resolve({ success: false, error: '响应解析失败' }); } }, onerror: () => resolve({ success: false, error: '网络错误' }), ontimeout: () => resolve({ success: false, error: '请求超时' }), }); }); }, // 答题结果上报 async reportAnswer(questionHash, isCorrect, question, answer, questionType) { const token = this.getToken(); if (!token || !questionHash) return { success: false, error: '参数不完整' }; return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'POST', url: `${API_BASE}/api/answer/report`, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, data: JSON.stringify({ question_hash: questionHash, is_correct: isCorrect, question: question, answer: answer, question_type: questionType }), timeout: 10000, onload: (resp) => { try { resolve(JSON.parse(resp.responseText)); } catch (e) { resolve({ success: false, error: '响应解析失败' }); } }, onerror: () => resolve({ success: false, error: '网络错误' }), ontimeout: () => resolve({ success: false, error: '请求超时' }), }); }); }, // v3.4.5: 上报学习通官方显示的正确答案(置信度拉到0.95) async correctAnswer(correctAnswer, questionHash, questionText, questionType) { const token = this.getToken(); if (!token || !questionHash) return; return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'POST', url: `${API_BASE}/api/answer/correct`, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, data: JSON.stringify({ question_hash: questionHash, correct_answer: correctAnswer, question_text: questionText || '', question_type: questionType || 'single' }), timeout: 10000, onload: (resp) => { try { resolve(JSON.parse(resp.responseText)); } catch (e) { resolve({}); } }, onerror: () => resolve({}), ontimeout: () => resolve({}), }); }); }, // 登录 async login(username, password) { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'POST', url: `${API_BASE}/api/login`, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ username, password }), timeout: 15000, onload: (resp) => { try { const data = JSON.parse(resp.responseText); if (data.success && data.data) { this.setToken(data.data.token); this.setUserInfo(data.data); } resolve(data); } catch (e) { resolve({ success: false, error: '响应解析失败' }); } }, onerror: () => resolve({ success: false, error: '网络错误' }), ontimeout: () => resolve({ success: false, error: '连接超时' }), }); }); }, // 注册 async register(username, password, inviteCode) { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'POST', url: `${API_BASE}/api/register`, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ username, password, inviteCode }), timeout: 15000, onload: (resp) => { try { const data = JSON.parse(resp.responseText); if (data.success && data.data) { this.setToken(data.data.token); this.setUserInfo(data.data); } resolve(data); } catch (e) { resolve({ success: false, error: '响应解析失败' }); } }, onerror: () => resolve({ success: false, error: '网络错误' }), ontimeout: () => resolve({ success: false, error: '连接超时' }), }); }); }, // 获取用户额度 async getUserQuota() { const token = this.getToken(); if (!token) return { loggedIn: false }; return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: `${API_BASE}/api/user/quota`, headers: { 'Authorization': `Bearer ${token}` }, timeout: 10000, onload: (resp) => { try { const data = JSON.parse(resp.responseText); if (data.success && data.data) { this.setUserInfo(data.data); resolve({ loggedIn: true, username: data.data.username, quota: data.data.quota, used: data.data.used, remaining: data.data.remaining, isWhitelisted: data.data.is_whitelisted, }); } else if (resp.status === 401) { this.clearAuth(); resolve({ loggedIn: false }); } else { resolve({ loggedIn: true, ...this.getUserInfo() }); } } catch (e) { resolve({ loggedIn: true, ...this.getUserInfo() }); } }, onerror: () => resolve({ loggedIn: true, ...this.getUserInfo() }), ontimeout: () => resolve({ loggedIn: true, ...this.getUserInfo() }), }); }); }, // 获取公开配置 async getConfig() { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: `${API_BASE}/api/config`, timeout: 10000, onload: (resp) => { try { resolve(JSON.parse(resp.responseText)); } catch { resolve({}); } }, onerror: () => resolve({}), ontimeout: () => resolve({}), }); }); }, // 版本检查 async checkVersion() { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: `${API_BASE}/api/version`, timeout: 10000, onload: (resp) => { try { resolve(JSON.parse(resp.responseText)); } catch { resolve({}); } }, onerror: () => resolve({}), ontimeout: () => resolve({}), }); }); }, // 加载字体映射表 async loadFontTable() { // 先检查缓存 const cached = GM_getValue('zhixue_fontTable', ''); const cacheTime = GM_getValue('zhixue_fontTableTime', 0); // 缓存24小时内有效 if (cached && Date.now() - cacheTime < 86400000) { try { return JSON.parse(cached); } catch {} } // 从服务器加载 return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: `${API_BASE}/static/table.json`, timeout: 30000, onload: (resp) => { try { const data = JSON.parse(resp.responseText); // 缓存 try { GM_setValue('zhixue_fontTable', resp.responseText); GM_setValue('zhixue_fontTableTime', Date.now()); } catch {} resolve(data); } catch { resolve(cached ? JSON.parse(cached) : null); } }, onerror: () => { try { resolve(JSON.parse(cached)); } catch { resolve(null); } }, ontimeout: () => { try { resolve(JSON.parse(cached)); } catch { resolve(null); } }, }); }); }, }; // ========================================== // 字体解密(v5.0.0 新增,脚本猫版适配) // 学习通使用自定义字体加密题目和选项文字 // HTML源码中的Unicode字符是乱码,通过自定义字体渲染后才显示正确文字 // .innerText 拿到的是乱码Unicode,导致答案文本匹配永远失败 // 解密后替换DOM中的乱码字符,使 .innerText 返回正确汉字 // // v3.3.8 彻底重写(参考 ScriptCat #4981 + 52pojie 方案): // - _replaceEncryptedText: 移除早期return,始终全文档扫描,不修改class // - _extractFontBase64: 收集所有@font-face,取加密字体(排除标准字体) // - _createCharMap: 扫描ASCII+CJK全范围,增加诊断日志 // - decrypt(): 增加 forestpolice.org 作为表备用源 // // v3.3.9 修复: // - _extractFontData: 同时支持base64内嵌和外部URL字体加载(GM_xmlhttpRequest获取) // - 增加诊断日志,console.warn确保可见 // - 修复备用源URL:移除scriptcat.org错误路径 // - decrypt()每个阶段都输出console.warn便于定位问题 // ========================================== const FontDecrypt = { _charMap: null, _tableLoaded: false, _observer: null, _decrypted: false, async decrypt() { // v3.3.9: 每个入口都输出warn日志确保可见 warn(`FontDecrypt.decrypt() 调用 (已有映射表: ${!!this._charMap}, 已解密: ${this._decrypted})`); // 如果已经解密过且映射表存在,直接增量替换 if (this._charMap) { const replaced = this._replaceEncryptedText(this._charMap); warn(`FontDecrypt 增量替换: ${replaced}处文本节点`); return true; } try { // v3.3.9: 使用新的_extractFontData同时支持base64和外部URL const fontSource = await this._extractFontData(); if (!fontSource) { warn('⚠️ 未检测到加密字体(检查CSS @font-face)'); return true; } if (typeof Typr === 'undefined' || typeof md5 === 'undefined') { warn('❌ Typr/md5 未加载'); return false; } const fontData = Typr.parse(this._base64ToUint8Array(fontSource)); if (!fontData || !fontData[0]) { warn('❌ TTF解析失败'); return false; } // 加载映射表(forestpolice主源 → api.zhixue.icu备用源) let table = await this._loadFallbackTable(); if (table && Object.keys(table).length < 50) { warn(`forestpolice映射表过少(${Object.keys(table).length}条),尝试备用源...`); table = null; } if (!table) { warn('forestpolice主源失败,尝试api.zhixue.icu...'); table = await Api.loadFontTable(); } if (!table) { warn('❌ 所有映射表源均失败'); return false; } warn(`映射表加载成功: ${Object.keys(table).length}条`); const { charMap, totalGlyphs, matchedCount } = this._createCharMap(fontData[0], table); if (matchedCount === 0) { warn(`❌ 字体解析完成但0匹配(字体含${totalGlyphs}个加密字形,表含${Object.keys(table).length}条)`); return false; } this._charMap = charMap; warn(`✅ 字体解密: ${matchedCount}/${totalGlyphs} 字形匹配 (${(matchedCount/totalGlyphs*100).toFixed(1)}%)`); // 全文档替换 const replaced = this._replaceEncryptedText(charMap); warn(`✅ DOM替换: ${replaced}处文本节点`); this._startObserver(); this._decrypted = true; return true; } catch (e) { err('❌ 字体解密异常:', e.message); return false; } }, /** * v3.3.9: 重写为_extractFontData,同时支持base64内嵌和外部URL字体 * 先检测页面是否有font-cxsecret字体加载 * 然后从@font-face中提取字体数据(base64或外部URL) */ async _extractFontData() { // ==== 四层检测 ==== // 第1层: styleSheets | 第2层: style标签 | 第3层: 外部CSS fetch | 第4层: 直接URL探针 // ================== const fontFaces = []; // ---- 第1层: styleSheets ---- try { for (const sheet of document.styleSheets) { try { const rules = sheet.cssRules || sheet.rules; if (!rules) continue; for (const rule of rules) { if (rule instanceof CSSFontFaceRule) { const family = rule.style.getPropertyValue('font-family') || ''; const src = rule.style.getPropertyValue('src') || ''; if (family && src) fontFaces.push({ family: family.replace(/['"]/g, '').trim(), src }); } } } catch (e) { /* 跨域跳过 */ } } } catch (e) {} warn(`[第1层] styleSheets: ${fontFaces.length}个@font-face`); // ---- 第2层: style标签 ---- if (fontFaces.length === 0) { const styles = document.querySelectorAll('style'); for (const el of styles) { const text = el.textContent || ''; const m = /@font-face\s*\{([^}]+)\}/gi; let fm; while ((fm = m.exec(text)) !== null) { const body = fm[1]; const fmMatch = body.match(/font-family\s*:\s*['"]?([^;'"}]+)['"]?\s*;?/i); const sm = body.match(/src\s*:\s*([^;}]+)\s*;?/i); if (fmMatch && sm && !fontFaces.some(f => f.family === fmMatch[1].trim())) { fontFaces.push({ family: fmMatch[1].trim(), src: sm[1].trim() }); } } } warn(`[第2层] style标签: ${fontFaces.length}个@font-face`); } // ---- 第3层: fetch外部CSS ---- if (fontFaces.length === 0) { const links = document.querySelectorAll('link[rel="stylesheet"]'); warn(`[第3层] 尝试${links.length}个外部CSS...`); for (const link of links) { try { const href = link.href; if (!href) continue; const resp = await fetch(href, { signal: AbortSignal.timeout(10000) }); if (!resp.ok) continue; const cssText = await resp.text(); warn(`[第3层] ${href} → ${cssText.length}字符`); const m = /@font-face\s*\{([^}]+)\}/gi; let fm; while ((fm = m.exec(cssText)) !== null) { const body = fm[1]; const fmMatch = body.match(/font-family\s*:\s*['"]?([^;'"}]+)['"]?\s*;?/i); const sm = body.match(/src\s*:\s*([^;}]+)\s*;?/i); if (fmMatch && sm && !fontFaces.some(f => f.family === fmMatch[1].trim())) { fontFaces.push({ family: fmMatch[1].trim(), src: sm[1].trim() }); } } } catch (e) { warn(`[第3层] fetch失败: ${e.message}`); } } warn(`[第3层] 外部CSS: ${fontFaces.length}个@font-face`); } // ---- 第4层: 直接URL探针 ---- if (fontFaces.length === 0) { warn(`[第4层] 探针已知font-cxsecret URL...`); const probeUrls = [ '//static.chaoxing.com/font/cxsecret.woff2', '//static.chaoxing.com/font/cxsecret.woff', '//static.chaoxing.com/font/cxsecret.ttf', '//pan-yz.chaoxing.com/font/cxsecret.woff2', '//pan-yz.chaoxing.com/font/cxsecret.woff', '//pan-yz.chaoxing.com/font/cxsecret.ttf', ]; for (const path of probeUrls) { const url = location.protocol + path; try { const result = await new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url, responseType: 'arraybuffer', timeout: 10000, onload: (r) => resolve(r.status >= 200 && r.status < 300 ? r : null), onerror: () => resolve(null), ontimeout: () => resolve(null), }); }); if (result) { warn(`✅ [第4层] 找到: ${url}`); fontFaces.push({ family: 'cxsecret', src: `url('${url}') format('woff2')` }); break; } } catch (e) { warn(`[第4层] ${url}失败: ${e.message}`); } } } if (fontFaces.length === 0) { warn('⚠️ [四层全失败] 未找到任何@font-face'); return null; } warn(`[字体] ${fontFaces.length}个: ${fontFaces.map(f => f.family).join(', ')}`); // ---- 识别加密字体 ---- // 排除已知标准/图标字体 const knownStandard = ['fontawesome', 'material icons', 'materialdesignicons', 'iconfont', 'glyphicons', 'source han', 'noto sans', 'noto serif', 'pingfang', 'helvetica', 'arial', 'sans-serif', 'serif', 'monospace', 'roboto', 'open sans', 'microsoft yahei', 'simsun', 'icomoon', 'fontello', 'dripicons', 'typicons', 'feather']; // 优先匹配加密字体关键词 let targetFont = fontFaces.find(f => ['secret', 'cxsecret', '加密', 'chinese', 'quote'].some(k => f.family.toLowerCase().includes(k)) ); // 没匹配到则选出现次数最多的非标准字体(CXChineseQuote出现4次,核心字体) if (!targetFont) { const customFonts = fontFaces.filter(f => !knownStandard.some(k => f.family.toLowerCase().includes(k)) ); if (customFonts.length === 0) { warn('⚠️ 所有字体均为标准字体'); return null; } // 按family出现次数降序,取出现最多的 const freq = {}; fontFaces.forEach(f => { freq[f.family] = (freq[f.family] || 0) + 1; }); targetFont = customFonts.sort((a, b) => (freq[b.family] || 0) - (freq[a.family] || 0))[0]; warn(`[降级] 取出现最多的: ${targetFont.family} (${freq[targetFont.family]}次)`); } else { warn(`[加密字体] ${targetFont.family}`); } // ---- 提取字体数据 ---- const src = targetFont.src; // 尝试base64 const b64Match = src.match(/url\(['"]?data:[^'"]*?base64,([A-Za-z0-9+/=]+)['"]?\)/i); if (b64Match) { warn(`✅ base64: ${b64Match[1].length}字节`); return b64Match[1]; } // 尝试外部URL(支持协议绝对、协议相对、无协议相对路径) const urlMatch = src.match(/url\(['"]?((?:https?:)?\/\/[^'"\)]+\.(?:woff2?|ttf|otf|eot)[^'"\)]*)['"]?\)/i) || src.match(/url\(['"]?((?:https?:)?\/\/[^'"\)]+)['"]?\)/i) || src.match(/url\(['"]?([^'"\)]+\.(?:woff2?|ttf))['"]?\)/i); if (urlMatch) { let fontUrl = urlMatch[1]; // 相对路径补全 if (!fontUrl.startsWith('http') && !fontUrl.startsWith('//')) { fontUrl = location.origin + '/' + fontUrl.replace(/^\.?\//, ''); } warn(`📥 下载: ${fontUrl}`); return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: fontUrl, responseType: 'arraybuffer', timeout: 30000, onload: (resp) => { if (resp.status >= 200 && resp.status < 300 && resp.response) { try { const bytes = new Uint8Array(resp.response); let binary = ''; for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]); const base64 = btoa(binary); warn(`✅ 下载成功: ${base64.length}字节`); resolve(base64); } catch (e) { err('❌ 转base64:', e.message); resolve(null); } } else { err(`❌ HTTP ${resp.status}`); resolve(null); } }, onerror: (e) => { err('❌ 下载错误:', e); resolve(null); }, ontimeout: () => { err('❌ 超时'); resolve(null); }, }); }); } warn(`⚠️ src无法解析: ${src.substring(0, 80)}`); return null; }, /** * v3.3.9: 备用映射表加载(只保留有效的 forestpolice.org) * 移除 scriptcat.org/lib/668/1.0/table.json(该URL实际返回TyprMd5.js而非映射表) */ async _loadFallbackTable() { const urls = [ 'https://www.forestpolice.org/ttf/2.0/table.json', ]; for (const url of urls) { try { const data = await new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: url, timeout: 15000, onload: (resp) => { try { resolve(JSON.parse(resp.responseText)); } catch { resolve(null); } }, onerror: () => resolve(null), ontimeout: () => resolve(null), }); }); if (data) { warn(`备用映射表加载成功: ${url} (${Object.keys(data).length}条)`); return data; } } catch (e) { /* 继续尝试下一个 */ } } return null; }, _base64ToUint8Array(base64) { const data = window.atob(base64); return new Uint8Array([...data].map(c => c.charCodeAt(0))); }, /** * v3.3.9: 增加decimal key回退(forestpolice表混合hex/numeric格式) */ _createCharMap(font, table) { const charMap = {}; let totalGlyphs = 0; let matchedCount = 0; const unmatchedSamples = []; // 扫描范围: ASCII (32-126) + 全角ASCII (65281-65374) + CJK (19968-40959) + CJK Ext A (13312-19903) const ranges = [ [32, 126], // ASCII 可打印字符 [65281, 65374], // 全角ASCII [19968, 40959], // CJK Unified Ideographs [13312, 19903], // CJK Extension A [131072, 173791], // CJK Extension B (可能部分) ]; for (const [start, end] of ranges) { for (let i = start; i <= end; i++) { try { const glyph = Typr.U.codeToGlyph(font, i); if (!glyph) continue; totalGlyphs++; const path = Typr.U.glyphToPath(font, glyph); // 与 ScriptCat #4981 / 52pojie 完全一致: md5(path).slice(24) const pathHash = md5(JSON.stringify(path)).slice(24); // v3.3.9: 同时尝试hex key和decimal key(forestpolice表混合格式) let decoded = table[pathHash]; if (!decoded) { // 尝试decimal key: 将hex字符串转为十进制数字 const decimalKey = parseInt(pathHash, 16); decoded = table[decimalKey]; } if (decoded) { charMap[String.fromCodePoint(i)] = String.fromCodePoint(Number(decoded)); matchedCount++; } else if (unmatchedSamples.length < 10) { // 收集前10个未匹配的样本用于诊断 unmatchedSamples.push({ encrypted: String.fromCodePoint(i), encryptedHex: i.toString(16), hashPrefix: pathHash.substring(0, 8) }); } } catch (e) { /* skip individual glyph errors */ } } } if (unmatchedSamples.length > 0) { warn(`未匹配样本(前10): ${unmatchedSamples.map(s => `${s.encrypted}(U+${s.encryptedHex})`).join(', ')}`); } return { charMap, totalGlyphs, matchedCount }; }, /** * v3.3.8: 完全重写 - 始终全文档TreeWalker扫描 * 不再依赖.font-cxsecret类,不再提前return */ _replaceEncryptedText(charMap) { const encChars = Object.keys(charMap); if (encChars.length === 0 || !document.body) return 0; // 构建高效替换:对每个文本节点,检查是否包含任意加密字符 const encSet = new Set(encChars); // 预编译正则:匹配任意加密字符 const encRegex = new RegExp(encChars.map(c => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g'); const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); const textNodes = []; while (walker.nextNode()) { const node = walker.currentNode; if (node.textContent && encChars.some(c => node.textContent.includes(c))) { textNodes.push(node); } } let replacedCount = 0; for (const node of textNodes) { const original = node.textContent; const replaced = original.replace(encRegex, (match) => charMap[match] || match); if (replaced !== original) { node.textContent = replaced; replacedCount++; } } // 同时处理.font-cxsecret元素(如果存在),移除class标记解密完成 const cxElements = document.querySelectorAll('.font-cxsecret'); cxElements.forEach(el => el.classList.remove('font-cxsecret')); return replacedCount; }, /** * MutationObserver: 监听新增DOM节点,自动解密 */ _startObserver() { if (this._observer) return; if (!this._charMap) return; const charMap = this._charMap; const encChars = Object.keys(charMap); if (encChars.length === 0) return; this._observer = new MutationObserver((mutations) => { let needsReplace = false; for (const mut of mutations) { if (mut.type === 'childList') { for (const node of mut.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE) { const text = node.textContent || ''; for (const enc of encChars) { if (text.includes(enc)) { needsReplace = true; break; } } if (needsReplace) break; } } } else if (mut.type === 'characterData') { needsReplace = true; } if (needsReplace) break; } if (needsReplace) { this._replaceEncryptedText(charMap); } }); this._observer.observe(document.body, { childList: true, subtree: true, characterData: true }); }, _stopObserver() { if (this._observer) { this._observer.disconnect(); this._observer = null; } } }; // ========================================== // 答案匹配引擎(6.x 原版,5层降级匹配) // ========================================== const Matcher = { /** * 将AI/题库返回的答案转换为选项字母列表 * v5.3.0: 参考chaoxing-ai彻底重写 * - L1: 纯字母格式匹配(ABC / A,B,C / A|B|C),严格要求答案去除分隔符后仅含字母 * - L2: 格式提取("答案是X") * - L3: 分隔符拆分逐片段匹配(支持单字母+文本混合) * - L4: 文本匹配(精确→去标点→模糊) * - L5: 索引兜底 */ resolveAnswer(answer, options, questionType) { const ans = String(answer).trim(); if (!ans || !options || options.length === 0) return []; // 计算合法字母范围 const validLetters = options.map(o => (o.letter || '').toUpperCase()).filter(l => /^[A-Z]$/.test(l)); const maxLetter = validLetters.length > 0 ? validLetters[validLetters.length - 1] : 'Z'; const optionCount = options.length; // ===== 第1层:纯字母格式匹配(最可靠) ===== // 参考chaoxing-ai: 只有去除分隔符后仅含字母才走这条路 // 避免从解释文本中误提取字母(如"ABC(注:由于...)"中括号外的A/B/C被正确提取, // 但括号内如果也有字母就不会被误抓——因为整体去除分隔符后不是纯字母) const cleaned = ans.replace(/[\s||,,;;、\n\r\t\u3000]+/g, ''); // 去分隔符 if (/^[A-Za-z]+$/.test(cleaned)) { // 纯字母格式:ABC → [A,B,C] 或 A,B,C → [A,B,C] const letters = cleaned.toUpperCase().split('').filter(l => l.charCodeAt(0) - 65 < optionCount); if (letters.length > 0) { if (questionType === 'single' && letters.length > 1) { log(`匹配L1(纯字母): ${ans} → ${letters.join('')} → 单选取首: ${letters[0]}`); return [letters[0]]; } log(`匹配L1(纯字母): ${ans} → ${letters.join('')}`); return letters; } } // ===== 第2层:格式提取 ===== const patterns = [ /(?:答案[是为]|正确答案|参考答案|选择|选)[::]*\s*([A-Z])/i, /(?:应该选|应选|当选)[::]*\s*([A-Z])/i, /^([A-Z])\s*[.。、))]/m ]; for (const p of patterns) { const m = ans.match(p); if (m && m[1].charCodeAt(0) - 65 < optionCount) { log(`匹配L2(格式提取): ${ans} → ${m[1]}`); return [m[1]]; } } // ===== 第3层:分隔符拆分逐片段匹配(参考chaoxing-ai cxaiFindMultipleIndices) ===== // 处理 "A|B|C"、"A,B,C"、"A、马克思主义 B、科学" 等混合格式 const parts = ans.split(/[||\n;;,,、]+/).map(p => p.trim()).filter(p => p.length > 0); if (parts.length >= 2) { const matched = []; for (const part of parts) { // 3a: 单字母片段 const singleLetter = part.match(/^([A-Ga-g])$/); if (singleLetter) { const l = singleLetter[1].toUpperCase(); if (l.charCodeAt(0) - 65 < optionCount && !matched.includes(l)) { matched.push(l); continue; } } // 3b: "A.xxx" 格式 const letterPrefix = part.match(/^([A-Ga-g])\s*[.。、::))]/); if (letterPrefix) { const l = letterPrefix[1].toUpperCase(); if (l.charCodeAt(0) - 65 < optionCount && !matched.includes(l)) { matched.push(l); continue; } } // 3c: 文本片段 → 匹配选项 const textHit = Matcher._matchTextToOption(part, options); if (textHit && !matched.includes(textHit)) { matched.push(textHit); } } if (matched.length > 0) { if (questionType === 'single' && matched.length > 1) matched.length = 1; log(`匹配L3(逐片段): "${ans}" → ${matched.join('')}`); return matched; } } // ===== 第4层:文本匹配(整体匹配) ===== const matched = []; // 4a: 精确文本 for (const opt of options) { if (opt.text && opt.text === ans && !matched.includes(opt.letter)) { matched.push(opt.letter); } } // 4b: 包含文本 if (matched.length === 0) { for (const opt of options) { if (opt.text && (opt.text.includes(ans) || ans.includes(opt.text)) && !matched.includes(opt.letter)) { matched.push(opt.letter); } } } // 4c: 去标点+模糊 if (matched.length === 0) { const normalize = s => s.replace(/[《》""''\s·、,。,.\-—_()()\[\]【】::!!??;;]/g, '').toLowerCase(); const normAns = normalize(ans); let bestOpt = null, bestScore = 0; for (const opt of options) { const normOpt = normalize(opt.text || ''); if (!normOpt || normOpt.length < 2) continue; if (normOpt.includes(normAns) || normAns.includes(normOpt)) { if (!matched.includes(opt.letter)) { matched.push(opt.letter); } } if (matched.length === 0) { const score = Matcher._similarity(normAns, normOpt); if (score > bestScore) { bestScore = score; bestOpt = opt; } } } if (matched.length === 0 && bestOpt && bestScore >= 0.6) { matched.push(bestOpt.letter); log(`匹配L4c(模糊${(bestScore*100).toFixed(0)}%): "${ans}" → ${bestOpt.letter}`); } } if (matched.length > 0) { if (questionType === 'single' && matched.length > 1) matched.length = 1; if (matched.length > 1) log(`匹配L4(文本): "${ans}" → ${matched.join('')}`); return matched; } // ===== 第5层:选项索引兜底 ===== const idxMatch = ans.match(/(\d+)/); if (idxMatch) { const idx = parseInt(idxMatch[1]) - 1; if (idx >= 0 && idx < optionCount) { log(`匹配L5(索引兜底): "${ans}" → 选项${options[idx].letter}`); return [options[idx].letter]; } } // ===== 第6层:最佳相似度兜底(单选题必选一个,不再跳题) ===== if (questionType === 'single') { const normalize = s => s.replace(/[《》""''\s·、,。,.\-—_()()\[\]【】::!!??;;]/g, '').toLowerCase(); const normAns = normalize(ans); let bestOpt = null, bestScore = 0; for (const opt of options) { const normOpt = normalize(opt.text || ''); if (!normOpt || normOpt.length < 2) continue; const score = Matcher._similarity(normAns, normOpt); if (score > bestScore) { bestScore = score; bestOpt = opt; } } if (bestOpt && bestScore >= 0) { log(`匹配L6(单选兜底${(bestScore*100).toFixed(0)}%): "${ans}" → ${bestOpt.letter} ${bestOpt.text}`); return [bestOpt.letter]; } } warn(`5层匹配全部失败: "${ans}", 选项: [${options.map(o => `${o.letter}:${o.text}`).join(', ')}]`); return []; }, /** 文本片段匹配到选项 */ _matchTextToOption(text, options) { const t = text.trim(); // 精确 for (const opt of options) { if (opt.text && opt.text === t) return opt.letter; } // 包含 for (const opt of options) { if (opt.text && (opt.text.includes(t) || t.includes(opt.text))) return opt.letter; } // 去标点 const normalize = s => s.replace(/[《》""''\s·、,。,.\-—_()()\[\]【】::!!??;;]/g, ''); const nt = normalize(t); for (const opt of options) { const no = normalize(opt.text || ''); if (no && nt && (no.includes(nt) || nt.includes(no))) return opt.letter; } return null; }, /** 字符级相似度(Jaccard系数 + 长序列匹配) */ _similarity(a, b) { if (!a || !b) return 0; if (a === b) return 1; // Jaccard字符集合相似度 const setA = new Set(a); const setB = new Set(b); let intersection = 0; for (const c of setA) { if (setB.has(c)) intersection++; } const jaccard = intersection / (setA.size + setB.size - intersection); // 最长公共子序列比 const lcsLen = Matcher._lcs(a, b).length; const lcsRatio = (2 * lcsLen) / (a.length + b.length); // 综合得分 return Math.max(jaccard, lcsRatio); }, /** 最长公共子序列 */ _lcs(a, b) { const m = a.length, n = b.length; const dp = Array.from({length: m + 1}, () => new Array(n + 1).fill(0)); for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { dp[i][j] = a[i-1] === b[j-1] ? dp[i-1][j-1] + 1 : Math.max(dp[i-1][j], dp[i][j-1]); } } // 回溯LCS let result = '', i = m, j = n; while (i > 0 && j > 0) { if (a[i-1] === b[j-1]) { result = a[i-1] + result; i--; j--; } else if (dp[i-1][j] > dp[i][j-1]) i--; else j--; } return result; } }; // ========================================== // 题目检测(6.x 原版) // ========================================== const Detectors = { detect() { const questions = []; const seenElements = new Set(); const seenTitles = new Set(); const containerSelectors = [ '.questionLi', '.TiMu', '.previewTiMu', '.singleQuesId', '[typename]' ]; for (const sel of containerSelectors) { const found = document.querySelectorAll(sel); found.forEach(container => { if (seenElements.has(container)) return; seenElements.add(container); const q = Detectors.parseQuestion(container, questions.length); if (q && q.text) { // 按题干去重,避免嵌套容器导致一道题算多次 const titleKey = q.text.trim().substring(0, 50); if (!seenTitles.has(titleKey)) { seenTitles.add(titleKey); questions.push(q); } } }); } if (questions.length > 0) { log(`检测到 ${questions.length} 道题目`); return questions; } return Detectors.detectByInputs(); }, parseQuestion(container, index) { let titleText = ''; const titleSelectors = [ '.fontLabel', 'h3.mark_name', '.Zy_TItle .clearfix', '.TiMu .qtContent', '.qtContent', '.Zy_TItle', 'h3', 'h2', 'h4', '.title', '.mark_name' ]; for (const sel of titleSelectors) { const el = container.querySelector(sel); if (el && el.innerText.trim().length > 1) { titleText = el.innerText.trim(); break; } } if (!titleText) { for (const child of container.children) { const text = child.innerText?.trim(); if (text && text.length > 3 && !child.classList.contains('answerBg') && !child.querySelector('.answerBg') && !/^\d+$/.test(text)) { titleText = text; break; } } } if (!titleText) { const fullText = container.innerText?.trim(); if (fullText && fullText.length > 5) { const firstLine = fullText.split('\n')[0].trim(); if (firstLine.length > 3) titleText = firstLine; } } if (!titleText) { warn(`题${index + 1}: 无法提取题干 (class: ${container.className})`); return null; } titleText = titleText.replace(/\n/g, ' ').replace(/^\d+[\.\、\)\]\s]+/, '').trim(); if (titleText.length < 2) { warn(`题${index + 1}: 题干过短 "${titleText}"`); return null; } const typeName = container.getAttribute('typename') || ''; let type = 'unknown'; if (typeName.includes('单选')) type = 'single'; else if (typeName.includes('多选')) type = 'multiple'; else if (typeName.includes('判断')) type = 'judge'; else if (typeName.includes('填空')) type = 'fill'; else if (typeName.includes('简答') || typeName.includes('论述')) type = 'essay'; const questionId = container.getAttribute('data') || container.getAttribute('questionid') || container.id || ''; const options = Detectors.extractOptions(container); if (type === 'unknown') { type = Detectors.guessType(container, options); } return { text: titleText, type, questionId, options, container, index }; }, guessType(container, options) { if (options.length === 0) { const hasTextarea = container.querySelector('textarea, [id^="answerEditor"], iframe[title="编辑器"]'); return hasTextarea ? 'fill' : 'unknown'; } if (options.length === 2) { const optTexts = options.map(o => o.text).join(''); if (/对|错|正确|错误|√|×/.test(optTexts)) return 'judge'; } const hasCheckbox = container.querySelector('input[type="checkbox"]') !== null || container.querySelector('[role="checkbox"]') !== null || container.querySelector('.check_answer_dx') !== null || container.querySelector('.num_option_dx') !== null; return hasCheckbox ? 'multiple' : 'single'; }, extractOptions(container) { const options = []; const answerBgs = container.querySelectorAll('.answerBg'); if (answerBgs.length > 0) { answerBgs.forEach((div, i) => { const labelSpan = div.querySelector('.num_option, .num_option_dx'); const letter = labelSpan?.getAttribute('data') || String.fromCharCode(65 + i); const pDiv = div.querySelector('.answer_p'); const text = pDiv ? pDiv.innerText.trim() : div.innerText.replace(/^[A-Z][\.\s、]*/, '').trim(); const isSelected = labelSpan?.classList.contains('check_answer') || labelSpan?.classList.contains('check_answer_dx') || div.classList.contains('cur'); options.push({ letter, text: text.substring(0, 80), element: div, isSelected }); }); return options; } const ariaOpts = container.querySelectorAll('[role="radio"], [role="checkbox"]'); if (ariaOpts.length > 0) { ariaOpts.forEach((el, i) => { const letter = el.getAttribute('data') || String.fromCharCode(65 + i); const text = el.innerText.trim().substring(0, 80); options.push({ letter, text, element: el, isSelected: el.getAttribute('aria-checked') === 'true' }); }); return options; } const lis = container.querySelectorAll('li.after, .options li, .num_option'); if (lis.length > 0) { lis.forEach((li, i) => { const letter = String.fromCharCode(65 + i); const text = li.innerText.trim().substring(0, 80); options.push({ letter, text, element: li, isSelected: false }); }); return options; } const labels = container.querySelectorAll('label'); const labelOpts = []; labels.forEach((label, i) => { const inp = label.querySelector('input[type="radio"], input[type="checkbox"]'); if (inp) { const letter = String.fromCharCode(65 + i); const text = label.innerText.trim().substring(0, 80); labelOpts.push({ letter, text, element: label, isSelected: inp.checked }); } }); if (labelOpts.length > 0) return labelOpts; return options; }, detectByInputs() { const questions = []; // === 检测填空题(text inputs / textareas / contenteditable)=== const fillContainers = new Set(); document.querySelectorAll('input[type="text"], input:not([type]), textarea, [contenteditable="true"]').forEach(el => { if (el.offsetWidth === 0 && el.offsetHeight === 0) return; // 隐藏元素跳过 // 找最近的题目容器 let container = el.closest('[typename], .questionLi, .TiMu, .previewTiMu, .singleQuesId, .mark_item, .question, div.row, li.clearfix'); if (!container) return; // 不在已知题目容器中,跳过(防顶层页面误检) if (fillContainers.has(container)) return; fillContainers.add(container); // 提取题干 let titleText = ''; for (const sel of ['h3', 'h2', 'h4', '.title', '.Zy_TItle', '.fontLabel', '.mark_name', '.mark_title', '.qtContent', 'label', 'span.subject_type', 'strong']) { const titleEl = container.querySelector(sel); if (titleEl && titleEl.innerText.trim().length > 1) { titleText = titleEl.innerText.trim(); break; } } if (!titleText) { // 取容器内非input的文本作为题干 const clone = container.cloneNode(true); clone.querySelectorAll('input, textarea, select, button').forEach(n => n.remove()); const text = clone.innerText?.trim().substring(0, 100); if (text && text.length > 3) titleText = text; } if (!titleText) titleText = '填空题' + (fillContainers.size); titleText = titleText.replace(/^\d+[\.\、\)\]\s]+/, '').trim(); // 收集填空输入框 const inputs = Array.from(container.querySelectorAll('input[type="text"], input:not([type]), textarea, [contenteditable="true"]')) .filter(inp => inp.offsetWidth > 0); questions.push({ text: titleText, type: 'fill', questionId: container.getAttribute('data') || container.getAttribute('questionid') || '', options: inputs.map((inp, i) => ({ letter: String.fromCharCode(65 + i), text: inp.placeholder || `填空${i + 1}`, element: inp, isSelected: !!inp.value?.trim() })), container, index: questions.length }); }); // === 检测UEditor填空题(百度UEditor富文本编辑器)=== // 学习通填空题使用UEditor,每个空位是一个独立的编辑器实例 // 编辑器ID格式:answerEditor{questionId}{blankIndex}(blankIndex从1开始) document.querySelectorAll('[id^="answerEditor"]').forEach(edDiv => { // 找最近的题目容器 let container = edDiv.closest('[typename], .questionLi, .TiMu, .previewTiMu, .singleQuesId, .mark_item, .question, div.row, li.clearfix'); if (!container) return; if (fillContainers.has(container)) return; // 已通过input方式检测到,不重复 fillContainers.add(container); // 提取题干 let titleText = ''; for (const sel of ['h3', 'h2', 'h4', '.title', '.Zy_TItle', '.fontLabel', '.mark_name', '.mark_title', '.qtContent', 'label', 'span.subject_type', 'strong']) { const titleEl = container.querySelector(sel); if (titleEl && titleEl.innerText.trim().length > 1) { titleText = titleEl.innerText.trim(); break; } } if (!titleText) { const clone = container.cloneNode(true); clone.querySelectorAll('[id^="answerEditor"], script, style').forEach(n => n.remove()); const text = clone.innerText?.trim().substring(0, 100); if (text && text.length > 3) titleText = text; } if (!titleText) titleText = '填空题' + (fillContainers.size); titleText = titleText.replace(/^\d+[\.\、\)\]\s]+/, '').trim(); // 从editor ID提取questionId let qId = container.getAttribute('data') || container.getAttribute('questionid') || ''; if (!qId) { const idMatch = edDiv.id.match(/answerEditor(\d+)\d/); if (idMatch) qId = idMatch[1]; } if (!qId) { // 从容器内的script标签提取 const scriptTag = container.querySelector('script'); if (scriptTag) { const m = scriptTag.textContent.match(/UE\.getEditor\("answerEditor(\d+)\d"/); if (m) qId = m[1]; } } const blanks = container.querySelectorAll('[id^="answerEditor"]'); questions.push({ text: titleText, type: 'fill', questionId: qId, options: Array.from(blanks).map((ed, i) => ({ letter: String.fromCharCode(65 + i), text: `填空${i + 1}`, element: ed, isSelected: false })), container, index: questions.length }); }); // === 检测选择题(radio / checkbox 分组)=== const radioGroups = {}; const checkboxGroups = {}; document.querySelectorAll('input[type="radio"]').forEach(inp => { if (!inp.name) return; if (!radioGroups[inp.name]) radioGroups[inp.name] = []; radioGroups[inp.name].push(inp); }); document.querySelectorAll('input[type="checkbox"]').forEach(inp => { if (!inp.name) return; if (!checkboxGroups[inp.name]) checkboxGroups[inp.name] = []; checkboxGroups[inp.name].push(inp); }); const groups = []; for (const [name, inputs] of Object.entries(radioGroups)) { if (inputs.length >= 2) groups.push({ type: 'single', inputs, name }); } for (const [name, inputs] of Object.entries(checkboxGroups)) { if (inputs.length >= 2) groups.push({ type: 'multiple', inputs, name }); } if (groups.length === 0 && questions.length === 0) return []; if (questions.length > 0) log(`兜底填空检测: ${questions.length} 个填空`); if (groups.length > 0) log(`兜底选择题检测: ${groups.length} 个选项组`); groups.forEach((group, idx) => { const firstInput = group.inputs[0]; let container = firstInput.closest('[typename], .questionLi, .TiMu, .previewTiMu, div, li'); let titleText = ''; if (container) { for (const sel of ['h3', 'h2', 'h4', '.title', '.Zy_TItle', '.fontLabel', '.mark_name']) { const h = container.querySelector(sel); if (h) { titleText = h.innerText.trim(); break; } } } if (!titleText) titleText = '题目' + (idx + 1); titleText = titleText.replace(/^\d+[\.\、\)\]\s]+/, '').trim(); const options = group.inputs.map((inp, j) => { const letter = String.fromCharCode(65 + j); const label = inp.closest('label, .answerBg, li, div'); const text = label ? label.innerText.trim().substring(0, 80) : letter; return { letter, text, element: label || inp, isSelected: inp.checked }; }); const qId = group.name.match(/(\d+)/)?.[1] || ''; questions.push({ text: titleText, type: group.type, questionId: qId, options, container: container || document.body, index: idx }); }); return questions; }, getEnvInfo() { return { url: location.href, hostname: location.hostname, isTop: isTop(), questionLi: document.querySelectorAll('.questionLi').length, tiMu: document.querySelectorAll('.TiMu').length, previewTiMu: document.querySelectorAll('.previewTiMu').length, singleQuesId: document.querySelectorAll('.singleQuesId').length, typename: document.querySelectorAll('[typename]').length, answerBg: document.querySelectorAll('.answerBg').length, radioInput: document.querySelectorAll('input[type="radio"]').length, checkboxInput: document.querySelectorAll('input[type="checkbox"]').length, textarea: document.querySelectorAll('textarea').length, ueditor: document.querySelectorAll('[id^="answerEditor"]').length, fillInputs: document.querySelectorAll('input[type="text"], input:not([type])').length, fillTextareas: document.querySelectorAll('textarea').length, fontLabel: document.querySelectorAll('.fontLabel').length, markName: document.querySelectorAll('.mark_name').length, bodyTextLen: document.body?.innerText?.length || 0, title: document.title, pageType: Detectors.detectPageType() }; }, detectPageType() { const p = location.pathname, u = location.href; if (p.includes('/exam/test/reVersionTestStartNew') || u.includes('reVersionTestStartNew')) return 'exam'; if (p.includes('/work/dowork') || p.includes('/work/doHomeWork') || p.includes('/mooc2/work/') || p.includes('/mooc-ans/mooc2/work/')) return 'work'; if (document.querySelector('.questionLi') || document.querySelector('.singleQuesId')) return 'work'; if (document.querySelector('.TiMu') || document.querySelector('.previewTiMu')) return 'exam'; return 'unknown'; } }; // ========================================== // IndexedDB 题库(6.x 原版) // ========================================== const Bank = { db: null, async init() { if (this.db) return; return new Promise((resolve, reject) => { const req = indexedDB.open('SmartStudyDB', 1); req.onupgradeneeded = e => { const db = e.target.result; if (!db.objectStoreNames.contains('questions')) db.createObjectStore('questions', { keyPath: 'hash' }); }; req.onsuccess = e => { this.db = e.target.result; resolve(); }; req.onerror = e => reject(e.target.error); }); }, _hash(text) { let h = 5381; const s = text.trim().replace(/\s+/g, ' '); for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) & 0x7FFFFFFF; return h.toString(36); }, _clean(raw) { return raw.replace(/^\d+[\.\、\)\]\s]+/, '').replace(/\s+/g, ' ').trim(); }, async query(text) { if (!this.db) await this.init(); const hash = this._hash(this._clean(text)); return new Promise(r => { const req = this.db.transaction('questions', 'readonly').objectStore('questions').get(hash); req.onsuccess = () => r(req.result); req.onerror = () => r(null); }); }, async save(data) { if (!this.db) await this.init(); const q = this._clean(data.question); const record = { hash: this._hash(q), question: q, answer: data.answer, type: data.type || 'unknown', platform: 'chaoxing', timestamp: Date.now() }; return new Promise(r => { const tx = this.db.transaction('questions', 'readwrite'); tx.objectStore('questions').put(record); tx.oncomplete = () => r(true); tx.onerror = () => r(false); }); }, async getStats() { if (!this.db) await this.init(); return new Promise(r => { const req = this.db.transaction('questions', 'readonly').objectStore('questions').count(); req.onsuccess = () => r({ total: req.result }); req.onerror = () => r({ total: 0 }); }); }, async exportAll() { if (!this.db) await this.init(); return new Promise(r => { const req = this.db.transaction('questions', 'readonly').objectStore('questions').getAll(); req.onsuccess = () => r(req.result); req.onerror = () => r([]); }); }, async importData(records) { if (!this.db) await this.init(); let added = 0; for (const rec of records) { const existing = await this.query(rec.question || rec.text); if (!existing) added++; await this.save({ question: rec.question || rec.text, answer: rec.answer, type: rec.type }); } return { added, total: records.length }; } }; // ==========================================// 倍速控制(6.x 原版) // ========================================== const Speed = { running: false, rate: 2, _timer: null, start(rate = 2) { this.rate = rate; this.running = true; this._apply(rate); this._startPoll(); log(`倍速 ${rate}x 已启动`); }, stop() { this.running = false; this._stopPoll(); this._resetVideos(); log('倍速已停止'); }, setRate(rate) { this.rate = rate; if (this.running) { this._apply(rate); this._startPoll(); } }, _apply(rate) { document.querySelectorAll('video, audio').forEach(el => { try { el.playbackRate = rate; el.defaultPlaybackRate = rate; } catch (e) {} }); document.querySelectorAll('iframe').forEach(f => { try { f.contentDocument?.querySelectorAll('video, audio').forEach(el => { el.playbackRate = rate; el.defaultPlaybackRate = rate; }); } catch (e) {} }); this._broadcast({ type: 'SPEED_SET', rate }); }, _resetVideos() { document.querySelectorAll('video, audio').forEach(el => { try { el.playbackRate = 1; el.defaultPlaybackRate = 1; } catch (e) {} }); document.querySelectorAll('iframe').forEach(f => { try { f.contentDocument?.querySelectorAll('video, audio').forEach(el => { el.playbackRate = 1; el.defaultPlaybackRate = 1; }); } catch (e) {} }); this._broadcast({ type: 'SPEED_SET', rate: 0 }); }, _startPoll() { this._stopPoll(); this._timer = setInterval(() => { if (this.running) this._apply(this.rate); }, 800); }, _stopPoll() { if (this._timer) { clearInterval(this._timer); this._timer = null; } }, _broadcast(data) { document.querySelectorAll('iframe').forEach(f => { try { f.contentWindow.postMessage({ channel: 'ZHIXUE_COMMAND', ...data }, '*'); } catch (e) {} }); } }; // ========================================== // 自动刷课(v2.0 重写 — 任务点驱动,不依赖视频事件) // 核心思路参考 unrival/chaoxing-ai 等开源方案: // pan-yz 播放器不是标准