// ==UserScript== // @name 谊安学助手 2.0 // @namespace yianxue-question-bank // @version 2.0.2 // @description 不绑定网址的题库助手:岗位搜索、答案全文核对、练习及桌面考试自动选择。 // @match *://*/* // @match file:///* // @run-at document-idle // @grant GM_getValue // @grant GM_setValue // @grant GM_setClipboard // @noframes // ==/UserScript== /* Third-party notices: JSZip and pako distributed under their MIT licenses. JSZip is dual licensed. At your choice you may use it under the MIT license *or* the GPLv3 license. The MIT License =============== Copyright (c) 2009-2016 Stuart Knightley, David Duponchel, Franz Buchinger, António Afonso Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. (The MIT License) Copyright (C) 2014-2017 by Vitaly Puzrin and Andrei Tuputcyn Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ (function () { const module = { exports: {} }; const exports = module.exports; /*! JSZip v3.10.1 - A JavaScript class for generating and reading zip files (c) 2009-2016 Stuart Knightley Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/main/LICENSE.markdown. JSZip uses the library pako released under the MIT license : https://github.com/nodeca/pako/blob/main/LICENSE */ !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).JSZip=e()}}(function(){return function s(a,o,h){function u(r,e){if(!o[r]){if(!a[r]){var t="function"==typeof require&&require;if(!e&&t)return t(r,!0);if(l)return l(r,!0);var n=new Error("Cannot find module '"+r+"'");throw n.code="MODULE_NOT_FOUND",n}var i=o[r]={exports:{}};a[r][0].call(i.exports,function(e){var t=a[r][1][e];return u(t||e)},i,i.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,e=0;e>2,s=(3&t)<<4|r>>4,a=1>6:64,o=2>4,r=(15&i)<<4|(s=p.indexOf(e.charAt(o++)))>>2,n=(3&s)<<6|(a=p.indexOf(e.charAt(o++))),l[h++]=t,64!==s&&(l[h++]=r),64!==a&&(l[h++]=n);return l}},{"./support":30,"./utils":32}],2:[function(e,t,r){"use strict";var n=e("./external"),i=e("./stream/DataWorker"),s=e("./stream/Crc32Probe"),a=e("./stream/DataLengthProbe");function o(e,t,r,n,i){this.compressedSize=e,this.uncompressedSize=t,this.crc32=r,this.compression=n,this.compressedContent=i}o.prototype={getContentWorker:function(){var e=new i(n.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new a("data_length")),t=this;return e.on("end",function(){if(this.streamInfo.data_length!==t.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),e},getCompressedWorker:function(){return new i(n.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(e,t,r){return e.pipe(new s).pipe(new a("uncompressedSize")).pipe(t.compressWorker(r)).pipe(new a("compressedSize")).withStreamInfo("compression",t)},t.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(e,t,r){"use strict";var n=e("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(){return new n("STORE compression")},uncompressWorker:function(){return new n("STORE decompression")}},r.DEFLATE=e("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(e,t,r){"use strict";var n=e("./utils");var o=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t){return void 0!==e&&e.length?"string"!==n.getTypeOf(e)?function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t[a])];return-1^e}(0|t,e,e.length,0):function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t.charCodeAt(a))];return-1^e}(0|t,e,e.length,0):0}},{"./utils":32}],5:[function(e,t,r){"use strict";r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(e,t,r){"use strict";var n=null;n="undefined"!=typeof Promise?Promise:e("lie"),t.exports={Promise:n}},{lie:37}],7:[function(e,t,r){"use strict";var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,i=e("pako"),s=e("./utils"),a=e("./stream/GenericWorker"),o=n?"uint8array":"array";function h(e,t){a.call(this,"FlateWorker/"+e),this._pako=null,this._pakoAction=e,this._pakoOptions=t,this.meta={}}r.magic="\b\0",s.inherits(h,a),h.prototype.processChunk=function(e){this.meta=e.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,e.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new i[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var t=this;this._pako.onData=function(e){t.push({data:e,meta:t.meta})}},r.compressWorker=function(e){return new h("Deflate",e)},r.uncompressWorker=function(){return new h("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(e,t,r){"use strict";function A(e,t){var r,n="";for(r=0;r>>=8;return n}function n(e,t,r,n,i,s){var a,o,h=e.file,u=e.compression,l=s!==O.utf8encode,f=I.transformTo("string",s(h.name)),c=I.transformTo("string",O.utf8encode(h.name)),d=h.comment,p=I.transformTo("string",s(d)),m=I.transformTo("string",O.utf8encode(d)),_=c.length!==h.name.length,g=m.length!==d.length,b="",v="",y="",w=h.dir,k=h.date,x={crc32:0,compressedSize:0,uncompressedSize:0};t&&!r||(x.crc32=e.crc32,x.compressedSize=e.compressedSize,x.uncompressedSize=e.uncompressedSize);var S=0;t&&(S|=8),l||!_&&!g||(S|=2048);var z=0,C=0;w&&(z|=16),"UNIX"===i?(C=798,z|=function(e,t){var r=e;return e||(r=t?16893:33204),(65535&r)<<16}(h.unixPermissions,w)):(C=20,z|=function(e){return 63&(e||0)}(h.dosPermissions)),a=k.getUTCHours(),a<<=6,a|=k.getUTCMinutes(),a<<=5,a|=k.getUTCSeconds()/2,o=k.getUTCFullYear()-1980,o<<=4,o|=k.getUTCMonth()+1,o<<=5,o|=k.getUTCDate(),_&&(v=A(1,1)+A(B(f),4)+c,b+="up"+A(v.length,2)+v),g&&(y=A(1,1)+A(B(p),4)+m,b+="uc"+A(y.length,2)+y);var E="";return E+="\n\0",E+=A(S,2),E+=u.magic,E+=A(a,2),E+=A(o,2),E+=A(x.crc32,4),E+=A(x.compressedSize,4),E+=A(x.uncompressedSize,4),E+=A(f.length,2),E+=A(b.length,2),{fileRecord:R.LOCAL_FILE_HEADER+E+f+b,dirRecord:R.CENTRAL_FILE_HEADER+A(C,2)+E+A(p.length,2)+"\0\0\0\0"+A(z,4)+A(n,4)+f+b+p}}var I=e("../utils"),i=e("../stream/GenericWorker"),O=e("../utf8"),B=e("../crc32"),R=e("../signature");function s(e,t,r,n){i.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=t,this.zipPlatform=r,this.encodeFileName=n,this.streamFiles=e,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}I.inherits(s,i),s.prototype.push=function(e){var t=e.meta.percent||0,r=this.entriesCount,n=this._sources.length;this.accumulate?this.contentBuffer.push(e):(this.bytesWritten+=e.data.length,i.prototype.push.call(this,{data:e.data,meta:{currentFile:this.currentFile,percent:r?(t+100*(r-n-1))/r:100}}))},s.prototype.openedSource=function(e){this.currentSourceOffset=this.bytesWritten,this.currentFile=e.file.name;var t=this.streamFiles&&!e.file.dir;if(t){var r=n(e,t,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:r.fileRecord,meta:{percent:0}})}else this.accumulate=!0},s.prototype.closedSource=function(e){this.accumulate=!1;var t=this.streamFiles&&!e.file.dir,r=n(e,t,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(r.dirRecord),t)this.push({data:function(e){return R.DATA_DESCRIPTOR+A(e.crc32,4)+A(e.compressedSize,4)+A(e.uncompressedSize,4)}(e),meta:{percent:100}});else for(this.push({data:r.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},s.prototype.flush=function(){for(var e=this.bytesWritten,t=0;t=this.index;t--)r=(r<<8)+this.byteAt(t);return this.index+=e,r},readString:function(e){return n.transformTo("string",this.readData(e))},readData:function(){},lastIndexOfSignature:function(){},readAndCheckSignature:function(){},readDate:function(){var e=this.readInt(4);return new Date(Date.UTC(1980+(e>>25&127),(e>>21&15)-1,e>>16&31,e>>11&31,e>>5&63,(31&e)<<1))}},t.exports=i},{"../utils":32}],19:[function(e,t,r){"use strict";var n=e("./Uint8ArrayReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.readData=function(e){this.checkOffset(e);var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(e,t,r){"use strict";var n=e("./DataReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.byteAt=function(e){return this.data.charCodeAt(this.zero+e)},i.prototype.lastIndexOfSignature=function(e){return this.data.lastIndexOf(e)-this.zero},i.prototype.readAndCheckSignature=function(e){return e===this.readData(4)},i.prototype.readData=function(e){this.checkOffset(e);var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./DataReader":18}],21:[function(e,t,r){"use strict";var n=e("./ArrayReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.readData=function(e){if(this.checkOffset(e),0===e)return new Uint8Array(0);var t=this.data.subarray(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./ArrayReader":17}],22:[function(e,t,r){"use strict";var n=e("../utils"),i=e("../support"),s=e("./ArrayReader"),a=e("./StringReader"),o=e("./NodeBufferReader"),h=e("./Uint8ArrayReader");t.exports=function(e){var t=n.getTypeOf(e);return n.checkSupport(t),"string"!==t||i.uint8array?"nodebuffer"===t?new o(e):i.uint8array?new h(n.transformTo("uint8array",e)):new s(n.transformTo("array",e)):new a(e)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(e,t,r){"use strict";r.LOCAL_FILE_HEADER="PK",r.CENTRAL_FILE_HEADER="PK",r.CENTRAL_DIRECTORY_END="PK",r.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK",r.ZIP64_CENTRAL_DIRECTORY_END="PK",r.DATA_DESCRIPTOR="PK\b"},{}],24:[function(e,t,r){"use strict";var n=e("./GenericWorker"),i=e("../utils");function s(e){n.call(this,"ConvertWorker to "+e),this.destType=e}i.inherits(s,n),s.prototype.processChunk=function(e){this.push({data:i.transformTo(this.destType,e.data),meta:e.meta})},t.exports=s},{"../utils":32,"./GenericWorker":28}],25:[function(e,t,r){"use strict";var n=e("./GenericWorker"),i=e("../crc32");function s(){n.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}e("../utils").inherits(s,n),s.prototype.processChunk=function(e){this.streamInfo.crc32=i(e.data,this.streamInfo.crc32||0),this.push(e)},t.exports=s},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(e,t,r){"use strict";var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataLengthProbe for "+e),this.propName=e,this.withStreamInfo(e,0)}n.inherits(s,i),s.prototype.processChunk=function(e){if(e){var t=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=t+e.data.length}i.prototype.processChunk.call(this,e)},t.exports=s},{"../utils":32,"./GenericWorker":28}],27:[function(e,t,r){"use strict";var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataWorker");var t=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,e.then(function(e){t.dataIsReady=!0,t.data=e,t.max=e&&e.length||0,t.type=n.getTypeOf(e),t.isPaused||t._tickAndRepeat()},function(e){t.error(e)})}n.inherits(s,i),s.prototype.cleanUp=function(){i.prototype.cleanUp.call(this),this.data=null},s.prototype.resume=function(){return!!i.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,n.delay(this._tickAndRepeat,[],this)),!0)},s.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(n.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},s.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var e=null,t=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":e=this.data.substring(this.index,t);break;case"uint8array":e=this.data.subarray(this.index,t);break;case"array":case"nodebuffer":e=this.data.slice(this.index,t)}return this.index=t,this.push({data:e,meta:{percent:this.max?this.index/this.max*100:0}})},t.exports=s},{"../utils":32,"./GenericWorker":28}],28:[function(e,t,r){"use strict";function n(e){this.name=e||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}n.prototype={push:function(e){this.emit("data",e)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(e){this.emit("error",e)}return!0},error:function(e){return!this.isFinished&&(this.isPaused?this.generatedError=e:(this.isFinished=!0,this.emit("error",e),this.previous&&this.previous.error(e),this.cleanUp()),!0)},on:function(e,t){return this._listeners[e].push(t),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(e,t){if(this._listeners[e])for(var r=0;r "+e:e}},t.exports=n},{}],29:[function(e,t,r){"use strict";var h=e("../utils"),i=e("./ConvertWorker"),s=e("./GenericWorker"),u=e("../base64"),n=e("../support"),a=e("../external"),o=null;if(n.nodestream)try{o=e("../nodejs/NodejsStreamOutputAdapter")}catch(e){}function l(e,o){return new a.Promise(function(t,r){var n=[],i=e._internalType,s=e._outputType,a=e._mimeType;e.on("data",function(e,t){n.push(e),o&&o(t)}).on("error",function(e){n=[],r(e)}).on("end",function(){try{var e=function(e,t,r){switch(e){case"blob":return h.newBlob(h.transformTo("arraybuffer",t),r);case"base64":return u.encode(t);default:return h.transformTo(e,t)}}(s,function(e,t){var r,n=0,i=null,s=0;for(r=0;r>>6:(r<65536?t[s++]=224|r>>>12:(t[s++]=240|r>>>18,t[s++]=128|r>>>12&63),t[s++]=128|r>>>6&63),t[s++]=128|63&r);return t}(e)},s.utf8decode=function(e){return h.nodebuffer?o.transformTo("nodebuffer",e).toString("utf-8"):function(e){var t,r,n,i,s=e.length,a=new Array(2*s);for(t=r=0;t>10&1023,a[r++]=56320|1023&n)}return a.length!==r&&(a.subarray?a=a.subarray(0,r):a.length=r),o.applyFromCharCode(a)}(e=o.transformTo(h.uint8array?"uint8array":"array",e))},o.inherits(a,n),a.prototype.processChunk=function(e){var t=o.transformTo(h.uint8array?"uint8array":"array",e.data);if(this.leftOver&&this.leftOver.length){if(h.uint8array){var r=t;(t=new Uint8Array(r.length+this.leftOver.length)).set(this.leftOver,0),t.set(r,this.leftOver.length)}else t=this.leftOver.concat(t);this.leftOver=null}var n=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;0<=r&&128==(192&e[r]);)r--;return r<0?t:0===r?t:r+u[e[r]]>t?r:t}(t),i=t;n!==t.length&&(h.uint8array?(i=t.subarray(0,n),this.leftOver=t.subarray(n,t.length)):(i=t.slice(0,n),this.leftOver=t.slice(n,t.length))),this.push({data:s.utf8decode(i),meta:e.meta})},a.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:s.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},s.Utf8DecodeWorker=a,o.inherits(l,n),l.prototype.processChunk=function(e){this.push({data:s.utf8encode(e.data),meta:e.meta})},s.Utf8EncodeWorker=l},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(e,t,a){"use strict";var o=e("./support"),h=e("./base64"),r=e("./nodejsUtils"),u=e("./external");function n(e){return e}function l(e,t){for(var r=0;r>8;this.dir=!!(16&this.externalFileAttributes),0==e&&(this.dosPermissions=63&this.externalFileAttributes),3==e&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||"/"!==this.fileNameStr.slice(-1)||(this.dir=!0)},parseZIP64ExtraField:function(){if(this.extraFields[1]){var e=n(this.extraFields[1].value);this.uncompressedSize===s.MAX_VALUE_32BITS&&(this.uncompressedSize=e.readInt(8)),this.compressedSize===s.MAX_VALUE_32BITS&&(this.compressedSize=e.readInt(8)),this.localHeaderOffset===s.MAX_VALUE_32BITS&&(this.localHeaderOffset=e.readInt(8)),this.diskNumberStart===s.MAX_VALUE_32BITS&&(this.diskNumberStart=e.readInt(4))}},readExtraFields:function(e){var t,r,n,i=e.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});e.index+4>>6:(r<65536?t[s++]=224|r>>>12:(t[s++]=240|r>>>18,t[s++]=128|r>>>12&63),t[s++]=128|r>>>6&63),t[s++]=128|63&r);return t},r.buf2binstring=function(e){return l(e,e.length)},r.binstring2buf=function(e){for(var t=new h.Buf8(e.length),r=0,n=t.length;r>10&1023,o[n++]=56320|1023&i)}return l(o,n)},r.utf8border=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;0<=r&&128==(192&e[r]);)r--;return r<0?t:0===r?t:r+u[e[r]]>t?r:t}},{"./common":41}],43:[function(e,t,r){"use strict";t.exports=function(e,t,r,n){for(var i=65535&e|0,s=e>>>16&65535|0,a=0;0!==r;){for(r-=a=2e3>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t[a])];return-1^e}},{}],46:[function(e,t,r){"use strict";var h,c=e("../utils/common"),u=e("./trees"),d=e("./adler32"),p=e("./crc32"),n=e("./messages"),l=0,f=4,m=0,_=-2,g=-1,b=4,i=2,v=8,y=9,s=286,a=30,o=19,w=2*s+1,k=15,x=3,S=258,z=S+x+1,C=42,E=113,A=1,I=2,O=3,B=4;function R(e,t){return e.msg=n[t],t}function T(e){return(e<<1)-(4e.avail_out&&(r=e.avail_out),0!==r&&(c.arraySet(e.output,t.pending_buf,t.pending_out,r,e.next_out),e.next_out+=r,t.pending_out+=r,e.total_out+=r,e.avail_out-=r,t.pending-=r,0===t.pending&&(t.pending_out=0))}function N(e,t){u._tr_flush_block(e,0<=e.block_start?e.block_start:-1,e.strstart-e.block_start,t),e.block_start=e.strstart,F(e.strm)}function U(e,t){e.pending_buf[e.pending++]=t}function P(e,t){e.pending_buf[e.pending++]=t>>>8&255,e.pending_buf[e.pending++]=255&t}function L(e,t){var r,n,i=e.max_chain_length,s=e.strstart,a=e.prev_length,o=e.nice_match,h=e.strstart>e.w_size-z?e.strstart-(e.w_size-z):0,u=e.window,l=e.w_mask,f=e.prev,c=e.strstart+S,d=u[s+a-1],p=u[s+a];e.prev_length>=e.good_match&&(i>>=2),o>e.lookahead&&(o=e.lookahead);do{if(u[(r=t)+a]===p&&u[r+a-1]===d&&u[r]===u[s]&&u[++r]===u[s+1]){s+=2,r++;do{}while(u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&sh&&0!=--i);return a<=e.lookahead?a:e.lookahead}function j(e){var t,r,n,i,s,a,o,h,u,l,f=e.w_size;do{if(i=e.window_size-e.lookahead-e.strstart,e.strstart>=f+(f-z)){for(c.arraySet(e.window,e.window,f,f,0),e.match_start-=f,e.strstart-=f,e.block_start-=f,t=r=e.hash_size;n=e.head[--t],e.head[t]=f<=n?n-f:0,--r;);for(t=r=f;n=e.prev[--t],e.prev[t]=f<=n?n-f:0,--r;);i+=f}if(0===e.strm.avail_in)break;if(a=e.strm,o=e.window,h=e.strstart+e.lookahead,u=i,l=void 0,l=a.avail_in,u=x)for(s=e.strstart-e.insert,e.ins_h=e.window[s],e.ins_h=(e.ins_h<=x&&(e.ins_h=(e.ins_h<=x)if(n=u._tr_tally(e,e.strstart-e.match_start,e.match_length-x),e.lookahead-=e.match_length,e.match_length<=e.max_lazy_match&&e.lookahead>=x){for(e.match_length--;e.strstart++,e.ins_h=(e.ins_h<=x&&(e.ins_h=(e.ins_h<=x&&e.match_length<=e.prev_length){for(i=e.strstart+e.lookahead-x,n=u._tr_tally(e,e.strstart-1-e.prev_match,e.prev_length-x),e.lookahead-=e.prev_length-1,e.prev_length-=2;++e.strstart<=i&&(e.ins_h=(e.ins_h<e.pending_buf_size-5&&(r=e.pending_buf_size-5);;){if(e.lookahead<=1){if(j(e),0===e.lookahead&&t===l)return A;if(0===e.lookahead)break}e.strstart+=e.lookahead,e.lookahead=0;var n=e.block_start+r;if((0===e.strstart||e.strstart>=n)&&(e.lookahead=e.strstart-n,e.strstart=n,N(e,!1),0===e.strm.avail_out))return A;if(e.strstart-e.block_start>=e.w_size-z&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):(e.strstart>e.block_start&&(N(e,!1),e.strm.avail_out),A)}),new M(4,4,8,4,Z),new M(4,5,16,8,Z),new M(4,6,32,32,Z),new M(4,4,16,16,W),new M(8,16,32,32,W),new M(8,16,128,128,W),new M(8,32,128,256,W),new M(32,128,258,1024,W),new M(32,258,258,4096,W)],r.deflateInit=function(e,t){return Y(e,t,v,15,8,0)},r.deflateInit2=Y,r.deflateReset=K,r.deflateResetKeep=G,r.deflateSetHeader=function(e,t){return e&&e.state?2!==e.state.wrap?_:(e.state.gzhead=t,m):_},r.deflate=function(e,t){var r,n,i,s;if(!e||!e.state||5>8&255),U(n,n.gzhead.time>>16&255),U(n,n.gzhead.time>>24&255),U(n,9===n.level?2:2<=n.strategy||n.level<2?4:0),U(n,255&n.gzhead.os),n.gzhead.extra&&n.gzhead.extra.length&&(U(n,255&n.gzhead.extra.length),U(n,n.gzhead.extra.length>>8&255)),n.gzhead.hcrc&&(e.adler=p(e.adler,n.pending_buf,n.pending,0)),n.gzindex=0,n.status=69):(U(n,0),U(n,0),U(n,0),U(n,0),U(n,0),U(n,9===n.level?2:2<=n.strategy||n.level<2?4:0),U(n,3),n.status=E);else{var a=v+(n.w_bits-8<<4)<<8;a|=(2<=n.strategy||n.level<2?0:n.level<6?1:6===n.level?2:3)<<6,0!==n.strstart&&(a|=32),a+=31-a%31,n.status=E,P(n,a),0!==n.strstart&&(P(n,e.adler>>>16),P(n,65535&e.adler)),e.adler=1}if(69===n.status)if(n.gzhead.extra){for(i=n.pending;n.gzindex<(65535&n.gzhead.extra.length)&&(n.pending!==n.pending_buf_size||(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending!==n.pending_buf_size));)U(n,255&n.gzhead.extra[n.gzindex]),n.gzindex++;n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),n.gzindex===n.gzhead.extra.length&&(n.gzindex=0,n.status=73)}else n.status=73;if(73===n.status)if(n.gzhead.name){i=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending===n.pending_buf_size)){s=1;break}s=n.gzindexi&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),0===s&&(n.gzindex=0,n.status=91)}else n.status=91;if(91===n.status)if(n.gzhead.comment){i=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending===n.pending_buf_size)){s=1;break}s=n.gzindexi&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),0===s&&(n.status=103)}else n.status=103;if(103===n.status&&(n.gzhead.hcrc?(n.pending+2>n.pending_buf_size&&F(e),n.pending+2<=n.pending_buf_size&&(U(n,255&e.adler),U(n,e.adler>>8&255),e.adler=0,n.status=E)):n.status=E),0!==n.pending){if(F(e),0===e.avail_out)return n.last_flush=-1,m}else if(0===e.avail_in&&T(t)<=T(r)&&t!==f)return R(e,-5);if(666===n.status&&0!==e.avail_in)return R(e,-5);if(0!==e.avail_in||0!==n.lookahead||t!==l&&666!==n.status){var o=2===n.strategy?function(e,t){for(var r;;){if(0===e.lookahead&&(j(e),0===e.lookahead)){if(t===l)return A;break}if(e.match_length=0,r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++,r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):3===n.strategy?function(e,t){for(var r,n,i,s,a=e.window;;){if(e.lookahead<=S){if(j(e),e.lookahead<=S&&t===l)return A;if(0===e.lookahead)break}if(e.match_length=0,e.lookahead>=x&&0e.lookahead&&(e.match_length=e.lookahead)}if(e.match_length>=x?(r=u._tr_tally(e,1,e.match_length-x),e.lookahead-=e.match_length,e.strstart+=e.match_length,e.match_length=0):(r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++),r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):h[n.level].func(n,t);if(o!==O&&o!==B||(n.status=666),o===A||o===O)return 0===e.avail_out&&(n.last_flush=-1),m;if(o===I&&(1===t?u._tr_align(n):5!==t&&(u._tr_stored_block(n,0,0,!1),3===t&&(D(n.head),0===n.lookahead&&(n.strstart=0,n.block_start=0,n.insert=0))),F(e),0===e.avail_out))return n.last_flush=-1,m}return t!==f?m:n.wrap<=0?1:(2===n.wrap?(U(n,255&e.adler),U(n,e.adler>>8&255),U(n,e.adler>>16&255),U(n,e.adler>>24&255),U(n,255&e.total_in),U(n,e.total_in>>8&255),U(n,e.total_in>>16&255),U(n,e.total_in>>24&255)):(P(n,e.adler>>>16),P(n,65535&e.adler)),F(e),0=r.w_size&&(0===s&&(D(r.head),r.strstart=0,r.block_start=0,r.insert=0),u=new c.Buf8(r.w_size),c.arraySet(u,t,l-r.w_size,r.w_size,0),t=u,l=r.w_size),a=e.avail_in,o=e.next_in,h=e.input,e.avail_in=l,e.next_in=0,e.input=t,j(r);r.lookahead>=x;){for(n=r.strstart,i=r.lookahead-(x-1);r.ins_h=(r.ins_h<>>=y=v>>>24,p-=y,0===(y=v>>>16&255))C[s++]=65535&v;else{if(!(16&y)){if(0==(64&y)){v=m[(65535&v)+(d&(1<>>=y,p-=y),p<15&&(d+=z[n++]<>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(d&(1<>>=y,p-=y,(y=s-a)>3,d&=(1<<(p-=w<<3))-1,e.next_in=n,e.next_out=s,e.avail_in=n>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24)}function s(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new I.Buf16(320),this.work=new I.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function a(e){var t;return e&&e.state?(t=e.state,e.total_in=e.total_out=t.total=0,e.msg="",t.wrap&&(e.adler=1&t.wrap),t.mode=P,t.last=0,t.havedict=0,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new I.Buf32(n),t.distcode=t.distdyn=new I.Buf32(i),t.sane=1,t.back=-1,N):U}function o(e){var t;return e&&e.state?((t=e.state).wsize=0,t.whave=0,t.wnext=0,a(e)):U}function h(e,t){var r,n;return e&&e.state?(n=e.state,t<0?(r=0,t=-t):(r=1+(t>>4),t<48&&(t&=15)),t&&(t<8||15=s.wsize?(I.arraySet(s.window,t,r-s.wsize,s.wsize,0),s.wnext=0,s.whave=s.wsize):(n<(i=s.wsize-s.wnext)&&(i=n),I.arraySet(s.window,t,r-n,i,s.wnext),(n-=i)?(I.arraySet(s.window,t,r-n,n,0),s.wnext=n,s.whave=s.wsize):(s.wnext+=i,s.wnext===s.wsize&&(s.wnext=0),s.whave>>8&255,r.check=B(r.check,E,2,0),l=u=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&u)<<8)+(u>>8))%31){e.msg="incorrect header check",r.mode=30;break}if(8!=(15&u)){e.msg="unknown compression method",r.mode=30;break}if(l-=4,k=8+(15&(u>>>=4)),0===r.wbits)r.wbits=k;else if(k>r.wbits){e.msg="invalid window size",r.mode=30;break}r.dmax=1<>8&1),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=3;case 3:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<>>8&255,E[2]=u>>>16&255,E[3]=u>>>24&255,r.check=B(r.check,E,4,0)),l=u=0,r.mode=4;case 4:for(;l<16;){if(0===o)break e;o--,u+=n[s++]<>8),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=5;case 5:if(1024&r.flags){for(;l<16;){if(0===o)break e;o--,u+=n[s++]<>>8&255,r.check=B(r.check,E,2,0)),l=u=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&(o<(d=r.length)&&(d=o),d&&(r.head&&(k=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),I.arraySet(r.head.extra,n,s,d,k)),512&r.flags&&(r.check=B(r.check,n,d,s)),o-=d,s+=d,r.length-=d),r.length))break e;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===o)break e;for(d=0;k=n[s+d++],r.head&&k&&r.length<65536&&(r.head.name+=String.fromCharCode(k)),k&&d>9&1,r.head.done=!0),e.adler=r.check=0,r.mode=12;break;case 10:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break e;o--,u+=n[s++]<>>=1)){case 0:r.mode=14;break;case 1:if(j(r),r.mode=20,6!==t)break;u>>>=2,l-=2;break e;case 2:r.mode=17;break;case 3:e.msg="invalid block type",r.mode=30}u>>>=2,l-=2;break;case 14:for(u>>>=7&l,l-=7&l;l<32;){if(0===o)break e;o--,u+=n[s++]<>>16^65535)){e.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&u,l=u=0,r.mode=15,6===t)break e;case 15:r.mode=16;case 16:if(d=r.length){if(o>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286>>=3,l-=3}for(;r.have<19;)r.lens[A[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=T(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=_,l-=_,r.lens[r.have++]=b;else{if(16===b){for(z=_+2;l>>=_,l-=_,0===r.have){e.msg="invalid bit length repeat",r.mode=30;break}k=r.lens[r.have-1],d=3+(3&u),u>>>=2,l-=2}else if(17===b){for(z=_+3;l>>=_)),u>>>=3,l-=3}else{for(z=_+7;l>>=_)),u>>>=7,l-=7}if(r.have+d>r.nlen+r.ndist){e.msg="invalid bit length repeat",r.mode=30;break}for(;d--;)r.lens[r.have++]=k}}if(30===r.mode)break;if(0===r.lens[256]){e.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=T(D,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=T(F,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){e.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===t)break e;case 20:r.mode=21;case 21:if(6<=o&&258<=h){e.next_out=a,e.avail_out=h,e.next_in=s,e.avail_in=o,r.hold=u,r.bits=l,R(e,c),a=e.next_out,i=e.output,h=e.avail_out,s=e.next_in,n=e.input,o=e.avail_in,u=r.hold,l=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(C=r.lencode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,r.length=b,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){e.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;g=(C=r.distcode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,64&g){e.msg="invalid distance code",r.mode=30;break}r.offset=b,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){e.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break e;if(d=c-h,r.offset>d){if((d=r.offset-d)>r.whave&&r.sane){e.msg="invalid distance too far back",r.mode=30;break}p=d>r.wnext?(d-=r.wnext,r.wsize-d):r.wnext-d,d>r.length&&(d=r.length),m=r.window}else m=i,p=a-r.offset,d=r.length;for(hd?(m=R[T+a[v]],A[I+a[v]]):(m=96,0),h=1<>S)+(u-=h)]=p<<24|m<<16|_|0,0!==u;);for(h=1<>=1;if(0!==h?(E&=h-1,E+=h):E=0,v++,0==--O[b]){if(b===w)break;b=t[r+a[v]]}if(k>>7)]}function U(e,t){e.pending_buf[e.pending++]=255&t,e.pending_buf[e.pending++]=t>>>8&255}function P(e,t,r){e.bi_valid>d-r?(e.bi_buf|=t<>d-e.bi_valid,e.bi_valid+=r-d):(e.bi_buf|=t<>>=1,r<<=1,0<--t;);return r>>>1}function Z(e,t,r){var n,i,s=new Array(g+1),a=0;for(n=1;n<=g;n++)s[n]=a=a+r[n-1]<<1;for(i=0;i<=t;i++){var o=e[2*i+1];0!==o&&(e[2*i]=j(s[o]++,o))}}function W(e){var t;for(t=0;t>1;1<=r;r--)G(e,s,r);for(i=h;r=e.heap[1],e.heap[1]=e.heap[e.heap_len--],G(e,s,1),n=e.heap[1],e.heap[--e.heap_max]=r,e.heap[--e.heap_max]=n,s[2*i]=s[2*r]+s[2*n],e.depth[i]=(e.depth[r]>=e.depth[n]?e.depth[r]:e.depth[n])+1,s[2*r+1]=s[2*n+1]=i,e.heap[1]=i++,G(e,s,1),2<=e.heap_len;);e.heap[--e.heap_max]=e.heap[1],function(e,t){var r,n,i,s,a,o,h=t.dyn_tree,u=t.max_code,l=t.stat_desc.static_tree,f=t.stat_desc.has_stree,c=t.stat_desc.extra_bits,d=t.stat_desc.extra_base,p=t.stat_desc.max_length,m=0;for(s=0;s<=g;s++)e.bl_count[s]=0;for(h[2*e.heap[e.heap_max]+1]=0,r=e.heap_max+1;r<_;r++)p<(s=h[2*h[2*(n=e.heap[r])+1]+1]+1)&&(s=p,m++),h[2*n+1]=s,u>=7;n>>=1)if(1&r&&0!==e.dyn_ltree[2*t])return o;if(0!==e.dyn_ltree[18]||0!==e.dyn_ltree[20]||0!==e.dyn_ltree[26])return h;for(t=32;t>>3,(s=e.static_len+3+7>>>3)<=i&&(i=s)):i=s=r+5,r+4<=i&&-1!==t?J(e,t,r,n):4===e.strategy||s===i?(P(e,2+(n?1:0),3),K(e,z,C)):(P(e,4+(n?1:0),3),function(e,t,r,n){var i;for(P(e,t-257,5),P(e,r-1,5),P(e,n-4,4),i=0;i>>8&255,e.pending_buf[e.d_buf+2*e.last_lit+1]=255&t,e.pending_buf[e.l_buf+e.last_lit]=255&r,e.last_lit++,0===t?e.dyn_ltree[2*r]++:(e.matches++,t--,e.dyn_ltree[2*(A[r]+u+1)]++,e.dyn_dtree[2*N(t)]++),e.last_lit===e.lit_bufsize-1},r._tr_align=function(e){P(e,2,3),L(e,m,z),function(e){16===e.bi_valid?(U(e,e.bi_buf),e.bi_buf=0,e.bi_valid=0):8<=e.bi_valid&&(e.pending_buf[e.pending++]=255&e.bi_buf,e.bi_buf>>=8,e.bi_valid-=8)}(e)}},{"../utils/common":41}],53:[function(e,t,r){"use strict";t.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(e,t,r){(function(e){!function(r,n){"use strict";if(!r.setImmediate){var i,s,t,a,o=1,h={},u=!1,l=r.document,e=Object.getPrototypeOf&&Object.getPrototypeOf(r);e=e&&e.setTimeout?e:r,i="[object process]"==={}.toString.call(r.process)?function(e){process.nextTick(function(){c(e)})}:function(){if(r.postMessage&&!r.importScripts){var e=!0,t=r.onmessage;return r.onmessage=function(){e=!1},r.postMessage("","*"),r.onmessage=t,e}}()?(a="setImmediate$"+Math.random()+"$",r.addEventListener?r.addEventListener("message",d,!1):r.attachEvent("onmessage",d),function(e){r.postMessage(a+e,"*")}):r.MessageChannel?((t=new MessageChannel).port1.onmessage=function(e){c(e.data)},function(e){t.port2.postMessage(e)}):l&&"onreadystatechange"in l.createElement("script")?(s=l.documentElement,function(e){var t=l.createElement("script");t.onreadystatechange=function(){c(e),t.onreadystatechange=null,s.removeChild(t),t=null},s.appendChild(t)}):function(e){setTimeout(c,0,e)},e.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),r=0;r 0 && missingAnswers.length === 0 && (expectedTotal === null || expectedTotal === questions.length), questions: questions }; } // 中和可能加载外部资源的属性后才交给 DOMParser;原值用于生成资源链接。 // 此函数不会把解析结果插入当前文档,也不执行保存网页中的任何脚本。 function parseHTML(html, sourceUrl) { if (typeof DOMParser === 'undefined') throw new Error('此功能需要浏览器 DOMParser。'); let inert = String(html).replace(/]*>[\s\S]*?<\/script\s*>/gi, '').replace(/]*>[\s\S]*?<\/style\s*>/gi, ''); inert = inert.replace(/<[a-z][^>]*>/gi, function (tag) { return tag.replace(/\s(src|srcset|href|poster|data|style|background|action|formaction)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, function (_, name, value) { return ' data-qb-original-' + name.toLowerCase() + '=' + value; }).replace(/\s(on[a-z]+)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, ''); }); const doc = new DOMParser().parseFromString(inert, 'text/html'); doc.__questionBankSourceURL = sourceUrl || ''; return doc; } function csvCell(value) { let text = String(value == null ? '' : value); // Excel 会在引号包裹的 CSV 单元格中依旧识别公式;前置单引号使其按文本打开。 if (/^[\s\uFEFF]*[=+\-@]/.test(text)) text = "'" + text; return '"' + text.replace(/"/g, '""') + '"'; } function toCSV(bank) { const labels = []; bank.questions.forEach(function (question) { question.options.forEach(function (option) { if (!labels.includes(option.label)) labels.push(option.label); }); }); const header = ['题号', '题型', '题目'].concat(labels.map(function (label) { return '选项' + label; }), ['全部选项', '参考答案', '答案对应选项', '题目解析', '图片链接', '来源网址', '题库名称', '完整性', '网页题数', '缺失答案题号']); const rows = bank.questions.map(function (question) { return [question.number, question.type, question.question].concat(labels.map(function (label) { const option = question.options.find(function (item) { return item.label === label; }); return option ? [option.text].concat(option.images).filter(Boolean).join('\n') : ''; }), [question.options.map(function (option) { return option.label + '. ' + option.text + (option.images.length ? '\n' + option.images.join('\n') : ''); }).join('\n'), question.answer, question.answerText, question.explanation, question.images.join('\n'), bank.sourceUrl, bank.title, bank.complete ? (bank.expectedTotal === null ? '当前页面完整(网页题数未知)' : '完整') : '不完整', bank.expectedTotal === null ? '未知' : bank.expectedTotal, bank.missingAnswers.join('、')]); }); return '\uFEFF' + [header].concat(rows).map(function (row) { return row.map(csvCell).join(','); }).join('\r\n') + '\r\n'; } function toJSON(bank) { return JSON.stringify(bank, null, 2); } function toText(bank) { const completeness = bank.complete ? (bank.expectedTotal === null ? '当前页面完整(网页题数未知)' : '完整') : '不完整'; const lines = [bank.title, '来源:' + (bank.sourceUrl || '本地网页'), '完整性:' + completeness, '题目:' + bank.total + ';有参考答案:' + bank.answered, '网页答题卡题数:' + (bank.expectedTotal === null ? '未知,只能核对当前页面' : bank.expectedTotal), '缺失答案题号:' + (bank.missingAnswers.length ? bank.missingAnswers.join('、') : '无')]; if (bank.expectedTotal !== null && bank.total < bank.expectedTotal) lines.push('缺失题目数:' + (bank.expectedTotal - bank.total)); lines.push(''); bank.questions.forEach(function (question) { lines.push(question.number + '. [' + question.type + '] ' + question.question); question.questionImages.forEach(function (url) { lines.push('题目图片:' + url); }); question.options.forEach(function (option) { lines.push(option.label + '. ' + option.text); option.images.forEach(function (url) { lines.push('选项图片:' + url); }); }); lines.push('参考答案:' + (question.answer || '网页未提供')); if (question.answerText) lines.push('对应选项:' + question.answerText); lines.push('题目解析:' + (question.explanation || '网页未提供')); question.explanationImages.forEach(function (url) { lines.push('解析图片:' + url); }); lines.push(''); }); return lines.join('\r\n'); } function typeCounts(bank) { return bank.questions.reduce(function (counts, question) { counts[question.type] = (counts[question.type] || 0) + 1; return counts; }, {}); } function filenameFor(bank, format) { const name = (bank.title || '练习题库').replace(/[<>:"/\\|?*\x00-\x1F]/g, '_').replace(/[. ]+$/, '').slice(0, 100) || '练习题库'; return name + '_题库_' + bank.total + '题' + (bank.complete ? '' : '_不完整') + '.' + format; } function download(bank, format) { format = String(format || 'csv').toLowerCase(); const formats = { csv: { encode: toCSV, mime: 'text/csv;charset=utf-8' }, json: { encode: toJSON, mime: 'application/json;charset=utf-8' }, txt: { encode: toText, mime: 'text/plain;charset=utf-8' } }; if (!formats[format]) throw new Error('导出格式仅支持 csv、json、txt。'); const filename = filenameFor(bank, format); const blob = new Blob([formats[format].encode(bank)], { type: formats[format].mime }); const url = URL.createObjectURL(blob); const link = root.document.createElement('a'); link.href = url; link.download = filename; root.document.body.appendChild(link); link.click(); link.remove(); // 留出时间让浏览器接管下载后再回收 URL。 root.setTimeout(function () { URL.revokeObjectURL(url); }, 30000); return filename; } root.QuestionBankCore = { version: '1.0.0', selectors: SELECTORS, extract: extract, parseHTML: parseHTML, savedSourceURL: savedSourceURL, resolveURL: resolveURL, toCSV: toCSV, toJSON: toJSON, toText: toText, typeCounts: typeCounts, filenameFor: filenameFor, download: download }; })(typeof window !== 'undefined' ? window : globalThis); /* Local question-bank import and storage. No network requests are made. */ (function (root) { 'use strict'; const ROLES = Object.freeze(['主操', '班长', '副操']); const BANK_KEY = 'yianxue.assistant.bank.v2'; const SETTING_PREFIX = 'yianxue.assistant.setting.v2.'; const MAX_BYTES = 60 * 1024 * 1024; let current = null; let importQueue = Promise.resolve(); function normalize(value) { // Answer identity must retain case, unit spelling, superscripts and punctuation. // Only presentation whitespace and invisible layout characters are cleaned. return String(value == null ? '' : value) .replace(/[\u200b-\u200d\u2060\uFEFF]/g, '') .replace(/\s+/g, ' ').trim(); } function normalizeType(value) { const type = String(value || '').trim().replace(/^[\[【]|[\]】]$/g, '').replace(/题$/, ''); return ({ 单选: '单选', 单项选择: '单选', single: '单选', radio: '单选', 多选: '多选', 多项选择: '多选', multiple: '多选', checkbox: '多选', 判断: '判断', 判断正误: '判断', boolean: '判断' })[type.toLowerCase()] || type; } function optionKey(options) { return JSON.stringify((options || []).map(option => normalize(option.text)).sort()); } function questionKey(record) { return JSON.stringify([normalizeType(record.type), normalize(record.question)]); } function hash(value) { let result = 2166136261; for (let i = 0; i < value.length; i++) { result ^= value.charCodeAt(i); result = Math.imul(result, 16777619); } return (result >>> 0).toString(36); } function storage() { // Userscript grants can be sandbox globals without being properties of window. const legacyGet = typeof GM_getValue === 'function' ? GM_getValue : root.GM_getValue; const legacySet = typeof GM_setValue === 'function' ? GM_setValue : root.GM_setValue; const modern = typeof GM !== 'undefined' && GM ? GM : root.GM; if (typeof legacyGet === 'function' && typeof legacySet === 'function') { return { name: 'userscript', get: (key, fallback) => Promise.resolve(legacyGet(key, fallback)), set: (key, value) => Promise.resolve(legacySet(key, value)) }; } if (modern && typeof modern.getValue === 'function' && typeof modern.setValue === 'function') { return { name: 'userscript', get: (key, fallback) => modern.getValue(key, fallback), set: (key, value) => modern.setValue(key, value) }; } return { name: 'origin', get: async function (key, fallback) { try { const raw = root.localStorage.getItem(key); return raw === null ? fallback : JSON.parse(raw); } catch (error) { throw new Error('无法读取此网址的本地存储:' + error.message); } }, set: async function (key, value) { try { root.localStorage.setItem(key, JSON.stringify(value)); } catch (error) { throw new Error('题库保存失败;请检查浏览器存储空间或油猴权限:' + error.message); } } }; } function snapshot(records, revision, storageName) { const counts = { 主操: 0, 班长: 0, 副操: 0 }; records.forEach(record => { counts[record.role]++; }); return { schemaVersion: 2, records, counts, total: records.length, revision: revision || 0, storage: storageName, updatedAt: new Date().toISOString() }; } // RFC 4180: quoted commas, escaped quotes, CRLF and embedded line breaks. function parseCSV(input) { const text = String(input).replace(/^\uFEFF/, ''); const rows = []; let row = [], field = '', quoted = false, closed = false, line = 1; function pushField() { row.push(field); field = ''; closed = false; } function pushRow() { pushField(); if (row.some(value => value !== '')) rows.push(row); row = []; } for (let i = 0; i < text.length; i++) { const ch = text[i]; if (quoted) { if (ch === '"') { if (text[i + 1] === '"') { field += '"'; i++; } else { quoted = false; closed = true; } } else { field += ch; if (ch === '\n') line++; } } else if (ch === ',') { pushField(); } else if (ch === '\r' || ch === '\n') { if (ch === '\r' && text[i + 1] === '\n') i++; pushRow(); line++; } else if (closed) { if (ch !== ' ' && ch !== '\t') throw new Error('CSV 第 ' + line + ' 行的引号后存在非法字符'); } else if (ch === '"') { if (field !== '') throw new Error('CSV 第 ' + line + ' 行有未转义的引号'); quoted = true; } else { field += ch; } } if (quoted) throw new Error('CSV 引号未闭合(第 ' + line + ' 行附近)'); if (field !== '' || row.length || closed) pushRow(); return rows; } function restoreCSVCell(value) { // toCSV prefixes one apostrophe only for a leading spreadsheet formula marker. // A real quote such as '引用, or two leading quotes, is retained. return String(value == null ? '' : value).replace(/^'(?=[\s\uFEFF]*[=+\-@])/, ''); } function roleFromPath(path) { return String(path || '').replace(/\\/g, '/').split('/').find(part => ROLES.includes(part)) || ''; } function courseFromFilename(name) { return String(name || '').replace(/\\/g, '/').split('/').pop().replace(/\.(csv|json)$/i, '').replace(/_题库_\d+题(?:_不完整)?$/, ''); } function boolValue(value) { const text = normalize(value).toLowerCase(); if (/^(正确|对|是|true|√|✓)$/.test(text)) return 'true'; if (/^(错误|错|否|false|×|✗)$/.test(text)) return 'false'; return ''; } function labelsForAnswer(answer, options, type) { const raw = String(answer == null ? '' : answer).trim().replace(/^(?:参考答案|正确答案|答案)\s*[::]\s*/, ''); const compact = raw.toUpperCase().replace(/[\s,,、;;|/]+/g, ''); if (/^[A-H]+$/.test(compact)) { if (new Set(compact).size !== compact.length) throw new Error('参考答案包含重复字母'); return Array.from(compact); } const exact = options.filter(option => normalize(option.text) === normalize(raw)); if (exact.length === 1) return [exact[0].label]; if (type === '判断' && boolValue(raw)) { const found = options.filter(option => boolValue(option.text) === boolValue(raw)); if (found.length === 1) return [found[0].label]; } throw new Error('参考答案无法对应选项:' + (raw || '答案为空')); } function validateRecord(raw, context) { context = context || {}; if (!raw || typeof raw !== 'object') throw new Error('题目记录格式不正确'); const role = String(raw.role || context.role || '').trim(); if (!ROLES.includes(role)) throw new Error('题库未标明岗位;请先选择主操、班长或副操再导入'); if (context.pathRole && raw.role && raw.role !== context.pathRole) throw new Error('题目岗位与 ZIP 目录岗位不一致'); const type = normalizeType(raw.type); if (!['单选', '多选', '判断'].includes(type)) throw new Error('题型不支持或缺失:' + (type || '空')); const question = String(raw.question == null ? '' : raw.question).trim(); if (!question) throw new Error('题干为空'); if (!Array.isArray(raw.options) || raw.options.length < 2 || raw.options.length > 8) throw new Error('每道题需要 2 至 8 个有效选项'); const options = raw.options.map((option, index) => { if (!option || typeof option !== 'object') throw new Error('选项格式不正确'); const label = String(option.label || String.fromCharCode(65 + index)).trim().toUpperCase().replace(/[..、::))]$/, ''); const text = String(option.text == null ? '' : option.text).trim(); if (!/^[A-H]$/.test(label) || !text) throw new Error('选项字母或文字为空/无效'); return { label, text, ...(Array.isArray(option.images) && option.images.length ? { images: option.images.map(String) } : {}) }; }); if (new Set(options.map(option => option.label)).size !== options.length) throw new Error('选项字母重复'); let answer = String(raw.answer == null ? '' : raw.answer).trim(); let labels; if (answer) labels = labelsForAnswer(answer, options, type); else if (Array.isArray(raw.correctTexts) && raw.correctTexts.length) { labels = raw.correctTexts.map(text => { const found = options.filter(option => normalize(option.text) === normalize(text)); if (found.length !== 1) throw new Error('正确答案文字无法唯一对应选项'); return found[0].label; }); } else throw new Error('参考答案为空'); if (labels.some(label => !options.some(option => option.label === label))) throw new Error('参考答案引用不存在的选项'); if ((type === '单选' || type === '判断') && labels.length !== 1) throw new Error('单选/判断题必须只有一个参考答案'); if (new Set(labels).size !== labels.length) throw new Error('参考答案重复'); const correctOptions = options.filter(option => labels.includes(option.label)); const correctTexts = correctOptions.map(option => option.text); if (Array.isArray(raw.correctTexts) && raw.correctTexts.length && JSON.stringify(raw.correctTexts.map(normalize).sort()) !== JSON.stringify(correctTexts.map(normalize).sort())) { throw new Error('正确答案文字与参考答案字母不一致'); } answer = correctOptions.map(option => option.label).join(''); const course = String(raw.course || context.course || '').trim(); if (!course) throw new Error('题库缺少课程名称'); const sourceUrl = String(raw.sourceUrl || context.sourceUrl || '').trim(); const number = raw.number == null || raw.number === '' ? context.number || 0 : Number(raw.number); if (!Number.isInteger(number) || number < 0) throw new Error('题号格式不正确'); const record = { role, course, sourceUrl, number, type, question, options, correctTexts, answer, answerText: correctOptions.map(option => option.label + '. ' + option.text).join(';'), explanation: String(raw.explanation == null ? '' : raw.explanation).trim(), sourceFile: String(raw.sourceFile || context.sourceFile || ''), ...(Array.isArray(raw.questionImages) && raw.questionImages.length ? { questionImages: raw.questionImages.map(String) } : {}) }; record.questionKey = questionKey(record); record.optionKey = optionKey(options); record.id = 'qb-' + hash(JSON.stringify([role, course, sourceUrl, number, record.questionKey, record.optionKey, correctTexts.map(normalize).sort()])); return record; } function readCSV(text, context) { const rows = parseCSV(text); if (rows.length < 2) throw new Error('CSV 没有题目记录'); const headers = rows[0].map(value => value.replace(/^\uFEFF/, '').trim()); if (new Set(headers).size !== headers.length) throw new Error('CSV 列名重复'); const map = new Map(headers.map((name, index) => [name, index])); if (!map.has('题目') || !map.has('参考答案')) throw new Error('CSV 缺少“题目”或“参考答案”列'); const read = (row, key) => map.has(key) ? restoreCSVCell(row[map.get(key)] || '') : ''; const optionColumns = headers.map((name, index) => ({ name, index })).filter(item => /^选项[A-H]$/.test(item.name)); return rows.slice(1).map((row, index) => { if (row.length !== headers.length) throw new Error('CSV 第 ' + (index + 2) + ' 条记录的列数与表头不一致'); let options = optionColumns.map(item => ({ label: item.name.slice(-1), text: restoreCSVCell(row[item.index] || '') })).filter(option => option.text.trim()); if (!options.length && map.has('全部选项')) { options = []; read(row, '全部选项').split(/\r?\n/).forEach(line => { const match = line.match(/^\s*([A-H])[..、::))]\s*(.*)$/); if (match) options.push({ label: match[1], text: match[2] }); else if (options.length) options[options.length - 1].text += '\n' + line; }); } try { return validateRecord({ role: read(row, '岗位') || read(row, '角色') || context.role, course: read(row, '题库名称') || read(row, '课程') || context.course, sourceUrl: read(row, '来源网址') || context.sourceUrl, number: read(row, '题号') || index + 1, type: read(row, '题型'), question: read(row, '题目'), options, answer: read(row, '参考答案'), explanation: read(row, '题目解析') }, context); } catch (error) { throw new Error('第 ' + (index + 2) + ' 条记录:' + error.message); } }); } function readJSON(text, context) { let data; try { data = JSON.parse(String(text).replace(/^\uFEFF/, '')); } catch (error) { throw new Error('JSON 格式错误:' + error.message); } let rows; if (Array.isArray(data)) rows = data; else if (data && Array.isArray(data.records)) rows = data.records; else if (data && Array.isArray(data.questions)) rows = data.questions; else throw new Error('JSON 应包含 questions 或 records 数组'); if (!rows.length) throw new Error('JSON 没有题目记录'); const base = { ...context, role: data.role || context.role, course: data.title || data.course || context.course, sourceUrl: data.sourceUrl || context.sourceUrl }; return rows.map((raw, index) => { try { return validateRecord(raw, { ...base, number: index + 1 }); } catch (error) { throw new Error('第 ' + (index + 1) + ' 道题:' + error.message); } }); } function isIgnored(name) { const parts = String(name).replace(/\\/g, '/').split('/'); return parts.some(part => /^(?:__MACOSX|\.DS_Store|\.~lock\.|~\$|\._)/i.test(part)); } function sourceScope(record) { return JSON.stringify([record.role, record.course, record.sourceUrl]); } function deduplicate(records) { const seen = new Set(); return records.filter(record => { const key = JSON.stringify([sourceScope(record), record.number, record.questionKey, record.optionKey, record.correctTexts.map(normalize).sort()]); if (seen.has(key)) return false; seen.add(key); return true; }); } async function load() { if (current) return current; const backend = storage(); let saved = await backend.get(BANK_KEY, null); if (typeof saved === 'string') { try { saved = JSON.parse(saved); } catch (_) { throw new Error('已缓存题库格式损坏;请重新导入题库'); } } if (!saved) return (current = snapshot([], 0, backend.name)); if (!Array.isArray(saved.records)) throw new Error('已缓存题库格式损坏;请重新导入题库'); const records = saved.records.map((record, index) => { try { return validateRecord(record, { number: index + 1 }); } catch (error) { throw new Error('已缓存题库的第 ' + (index + 1) + ' 道题无效;请重新导入:' + error.message); } }); current = snapshot(deduplicate(records), Number(saved.revision) || 0, backend.name); if (saved.updatedAt) current.updatedAt = saved.updatedAt; return current; } async function fileBytes(file) { if (typeof file.arrayBuffer === 'function') return file.arrayBuffer(); if (typeof root.FileReader === 'function') return new Promise((resolve, reject) => { const reader = new root.FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = () => reject(new Error('无法读取导入文件')); reader.readAsArrayBuffer(file); }); throw new Error('浏览器不支持文件读取'); } function decodeCSV(bytes) { const decoder = root.TextDecoder || (typeof TextDecoder !== 'undefined' ? TextDecoder : null); if (!decoder) throw new Error('浏览器不支持 TextDecoder'); try { return new decoder('utf-8', { fatal: true }).decode(bytes); } catch (_) { return new decoder('gb18030', { fatal: true }).decode(bytes); } } async function performImport(files, options) { const selected = Array.from(files || []).filter(file => !isIgnored(file.webkitRelativePath || file.name)); if (!selected.length) throw new Error('请选择 ZIP、CSV 或 JSON 题库文件'); if (options.role && !ROLES.includes(options.role)) throw new Error('岗位只能选择主操、班长或副操'); const zipFiles = selected.filter(file => /\.zip$/i.test(file.name || '')); if (zipFiles.length && (zipFiles.length !== 1 || selected.length !== 1)) throw new Error('ZIP 全量导入时请只选择一个 ZIP 文件'); const incoming = []; let bytesRead = 0; function accept(text, name, pathRole) { bytesRead += text.length * 2; if (bytesRead > MAX_BYTES) throw new Error('题库解压后超过 60 MB,请拆分导入'); const context = { role: pathRole || options.role || '', pathRole, course: courseFromFilename(name), sourceFile: name }; try { incoming.push(...(/\.csv$/i.test(name) ? readCSV(text, context) : readJSON(text, context))); } catch (error) { throw new Error(name + ':' + error.message); } } if (zipFiles.length) { if (!root.JSZip || typeof root.JSZip.loadAsync !== 'function') throw new Error('ZIP 解析组件未加载,请重新安装完整脚本'); const file = zipFiles[0]; if (file.size > MAX_BYTES) throw new Error('ZIP 文件超过 60 MB'); const zip = await root.JSZip.loadAsync(await fileBytes(file)); const entries = Object.values(zip.files).filter(entry => !entry.dir && !isIgnored(entry.name) && /\.(csv|json)$/i.test(entry.name)); if (!entries.length) throw new Error('ZIP 内没有 CSV 或 JSON 题库'); if (entries.length > 2000) throw new Error('ZIP 内题库文件数量过多'); let estimatedSize = 0; for (const entry of entries) { if (entry._data && entry._data.uncompressedSize) estimatedSize += entry._data.uncompressedSize; if (estimatedSize > MAX_BYTES) throw new Error('题库解压后超过 60 MB,请拆分导入'); const text = decodeCSV(await entry.async('uint8array')); accept(text, entry.name, roleFromPath(entry.name)); } } else { for (const file of selected) { const name = file.webkitRelativePath || file.name || ''; if (!/\.(csv|json)$/i.test(name)) throw new Error('仅支持 ZIP、CSV、JSON 文件:' + name); if (file.size > MAX_BYTES) throw new Error('文件超过 60 MB:' + name); accept(decodeCSV(await fileBytes(file)), name, roleFromPath(name)); } } if (!incoming.length) throw new Error('题库没有有效题目'); const backend = storage(); let previous; try { previous = await load(); } catch (error) { if (!zipFiles.length) throw error; previous = snapshot([], 0, backend.name); } const scopes = new Set(incoming.map(sourceScope)); const retained = zipFiles.length ? [] : previous.records.filter(record => !scopes.has(sourceScope(record))); const next = snapshot(deduplicate(retained.concat(incoming)), previous.revision + 1, backend.name); // Only publish after every record has passed validation and storage succeeded. await backend.set(BANK_KEY, next); current = next; return next; } function importFiles(files, options) { const selection = Array.from(files || []); const settings = { ...(options || {}) }; const job = importQueue.then(() => performImport(selection, settings)); importQueue = job.catch(() => {}); return job; } async function getSetting(key, fallback) { return storage().get(SETTING_PREFIX + String(key), fallback); } async function setSetting(key, value) { await storage().set(SETTING_PREFIX + String(key), value); return value; } const api = Object.freeze({ version: '2.0.0', roles: ROLES, load, importFiles, getSetting, setSetting, parseCSV, validateRecord, normalize, normalizeType, optionKey, questionKey }); root.YianBankStore = api; if (typeof module !== 'undefined' && module.exports) module.exports = api; })(typeof window !== 'undefined' ? window : globalThis); /* Exact answer matching and role-scoped local search. */ (function (root) { 'use strict'; const ROLES = ['主操', '班长', '副操']; const cache = new WeakMap(); function normalize(value) { // Strict answer identity: MPa/mPa, m²/m2 and Ⅰ/I are distinct. return String(value == null ? '' : value) .replace(/[\u200b-\u200d\u2060\uFEFF]/g, '') .replace(/\s+/g, ' ').trim(); } // Search is intentionally more tolerant; these keys never authorize clicks. function searchNormalize(value) { return normalize(value).normalize('NFKC').replace(/[−﹣]/g, '-').toLowerCase(); } function courseNormalize(value) { return searchNormalize(value); } function normalizeType(value) { const type = String(value || '').trim().replace(/^[\[【]|[\]】]$/g, '').replace(/题$/, ''); return ({ 单选: '单选', 单项选择: '单选', single: '单选', radio: '单选', 多选: '多选', 多项选择: '多选', multiple: '多选', checkbox: '多选', 判断: '判断', 判断正误: '判断', boolean: '判断' })[type.toLowerCase()] || type; } function optionKey(options) { return JSON.stringify((options || []).map(option => normalize(option.text)).sort()); } function questionKey(record) { return JSON.stringify([normalizeType(record.type), normalize(record.question)]); } function correctKey(record) { return JSON.stringify((record.correctTexts || []).map(normalize).sort()); } function groupKey(record) { return questionKey(record) + '\n' + optionKey(record.options); } function normalizedLabel(value) { return String(value || '').normalize('NFKC').trim().toUpperCase().replace(/[.、::)]$/, ''); } function getIndex(snapshot) { if (!snapshot || !Array.isArray(snapshot.records)) return new Map(); const saved = cache.get(snapshot); if (saved && saved.revision === snapshot.revision && saved.records === snapshot.records) return saved.index; const index = new Map(); snapshot.records.forEach(record => { if (!ROLES.includes(record.role)) return; const key = record.role + '\n' + groupKey(record); if (!index.has(key)) index.set(key, []); index.get(key).push(record); }); cache.set(snapshot, { revision: snapshot.revision, records: snapshot.records, index }); return index; } function sourceIdentity(value) { if (!value) return ''; try { const url = new URL(value, 'https://local.invalid'); const route = url.hash.startsWith('#/') ? new URL(url.hash.slice(1), url.origin) : url; for (const params of [route.searchParams, url.searchParams]) { for (const [key, id] of params) { if (/^(course_?id|exam_?id|paper_?id|training_?id|lesson_?id)$/i.test(key) && id) return key.toLowerCase().replace(/_/g, '') + ':' + id; } } const exam = route.pathname.match(/\/exam\/([^/]+)\/(?:questions|exercise)\/?$/i); if (exam) return 'examid:' + decodeURIComponent(exam[1]); // A source path works after a domain/IP change, while title parameters do not identify a course. const params = new URLSearchParams(route.search); ['title', 'flag', 'name'].forEach(key => params.delete(key)); params.sort(); return route.pathname + (params.size ? '?' + params.toString() : ''); } catch (_) { return String(value); } } function result(status, reason, records, labels, texts) { return { status, reason, records: records || [], correctLabels: labels || [], correctTexts: texts || [] }; } function match(question, context, snapshot) { context = context || {}; if (!ROLES.includes(context.role)) return result('unsupported', '请先选择主操、班长或副操'); if (!question || !['单选', '多选', '判断'].includes(normalizeType(question.type))) return result('unsupported', '当前题型暂不支持自动选择'); if (!normalize(question.question) || !Array.isArray(question.options) || question.options.length < 2) return result('unsupported', '题干或选项尚未完整加载'); if ((question.questionImages && question.questionImages.length) || question.options.some(option => !normalize(option.text) || (option.images && option.images.length))) return result('unsupported', '含图片的题目需要人工核对'); const currentTexts = question.options.map(option => normalize(option.text)); const currentLabels = question.options.map(option => normalizedLabel(option.label)); if (new Set(currentTexts).size !== currentTexts.length || new Set(currentLabels).size !== currentLabels.length || currentLabels.some(label => !/^[A-H]$/.test(label))) return result('unsupported', '当前选项文字或字母重复,无法唯一映射'); let records = getIndex(snapshot).get(context.role + '\n' + groupKey(question)) || []; if (!records.length) return result('missing', '所选岗位题库没有题型、题干和全部选项均一致的记录'); let sourceMatched = false; const identity = sourceIdentity(context.sourceUrl); if (identity) { const exactSource = records.filter(record => record.sourceUrl && sourceIdentity(record.sourceUrl) === identity); if (exactSource.length) { records = exactSource; sourceMatched = true; } } const course = courseNormalize(context.course); if (course) { const exactCourse = records.filter(record => courseNormalize(record.course) === course); if (exactCourse.length) { records = exactCourse; sourceMatched = true; } } const answers = new Set(records.map(correctKey)); if (answers.size > 1) return result('conflict', '相同题干与选项存在不同答案;当前课程来源未能唯一确定', records); const wanted = records[0].correctTexts || []; const wantedKeys = wanted.map(normalize); if (!wantedKeys.length || new Set(wantedKeys).size !== wantedKeys.length || wantedKeys.some(text => !currentTexts.includes(text))) return result('unsupported', '题库参考答案无法唯一对应当前选项', records); const selected = question.options.filter(option => wantedKeys.includes(normalize(option.text))); if (normalizeType(question.type) !== '多选' && selected.length !== 1) return result('unsupported', '单选/判断题的参考答案数量不正确', records); // Keep label/text pairs in current-page order, but obtain each text from bank evidence. const evidenceTexts = selected.map(option => wanted.find(text => normalize(text) === normalize(option.text))); return result('matched', sourceMatched ? '岗位、课程、题干及选项完全匹配' : '岗位、题干及选项完全匹配,所有来源答案一致', records, selected.map(option => normalizedLabel(option.label)), evidenceTexts); } function search(query, settings, snapshot) { settings = settings || {}; const role = settings.role; const empty = reason => ({ items: [], total: 0, reason, role: role || '', query: String(query || ''), hasMore: false }); if (!ROLES.includes(role)) return empty('请先选择主操、班长或副操'); const fullQuery = searchNormalize(query); if (!fullQuery) return empty('请输入题干、关键词或选项文字'); const tokens = fullQuery.split(/\s+/).filter(Boolean); const course = courseNormalize(settings.course); const records = snapshot && Array.isArray(snapshot.records) ? snapshot.records : []; const groups = new Map(), conflicts = new Map(); records.forEach(record => { if (record.role !== role || (course && courseNormalize(record.course) !== course)) return; const baseKey = groupKey(record), answerKey = correctKey(record), key = baseKey + '\n' + answerKey; if (!conflicts.has(baseKey)) conflicts.set(baseKey, new Set()); conflicts.get(baseKey).add(answerKey); if (!groups.has(key)) groups.set(key, { ...record, sources: [], conflict: false, sourceCount: 0, _baseKey: baseKey, _sources: new Set() }); const group = groups.get(key); const source = { role: record.role, course: record.course, sourceUrl: record.sourceUrl || '', number: record.number }; const sourceKey = JSON.stringify(source); if (!group._sources.has(sourceKey)) { group._sources.add(sourceKey); group.sources.push(source); group.sourceCount++; } }); const items = []; groups.forEach(group => { const stem = searchNormalize(group.question); const optionText = (group.options || []).map(option => searchNormalize(option.text)).join('\n'); const answerText = (group.correctTexts || []).map(searchNormalize).join('\n'); const haystack = stem + '\n' + optionText + '\n' + answerText; if (!tokens.every(token => haystack.includes(token))) return; let score = stem === fullQuery ? 10000 : stem.includes(fullQuery) ? 5000 : 0; tokens.forEach(token => { score += stem.includes(token) ? 100 : optionText.includes(token) ? 30 : 10; }); group.conflict = conflicts.get(group._baseKey).size > 1; delete group._baseKey; delete group._sources; items.push({ ...group, score }); }); items.sort((left, right) => right.score - left.score || left.question.localeCompare(right.question, 'zh-CN') || left.course.localeCompare(right.course, 'zh-CN') || left.number - right.number); const total = items.length; const limit = Math.max(1, Math.min(100, Number(settings.limit) || 20)); const offset = Math.max(0, Math.floor(Number(settings.offset) || 0)); return { items: items.slice(offset, offset + limit), total, role, query: String(query), reason: total ? '' : '当前岗位未找到符合条件的题目', hasMore: offset + limit < total }; } const api = Object.freeze({ version: '2.0.0', normalize, searchNormalize, normalizeType, optionKey, questionKey, sourceIdentity, match, search }); root.YianAnswerMatcher = api; if (typeof module !== 'undefined' && module.exports) module.exports = api; })(typeof window !== 'undefined' ? window : globalThis); /* 仅解除正文的选择 / 右键内联限制;保留并恢复网页自身的设置。 */ (function (global) { 'use strict'; const EVENT_FIELDS = ['onselectstart', 'oncontextmenu']; function inheritedDescriptor(node, name) { let prototype = Object.getPrototypeOf(node); while (prototype) { const descriptor = Object.getOwnPropertyDescriptor(prototype, name); if (descriptor) return descriptor; prototype = Object.getPrototypeOf(prototype); } return null; } function create() { const doc = global.document; const records = new Map(); let enabled = false; let destroyed = false; let style = null; let observer = null; function readNative(record, field) { try { if (field.native && typeof field.native.get === 'function') return field.native.get.call(record.node); return null; } catch (_) { return null; } } function writeNative(record, field, value) { try { if (field.native && typeof field.native.set === 'function') field.native.set.call(record.node, value); } catch (_) { /* 不覆盖不允许修改的网页字段。 */ } } function removeRestriction(record, field) { if (record.node.hasAttribute(field.name)) record.removeAttribute.call(record.node, field.name); writeNative(record, field, null); } function rememberAttribute(record, field) { field.hasAttribute = record.node.hasAttribute(field.name); field.attributeValue = record.node.getAttribute(field.name); field.intendedProperty = readNative(record, field); field.propertyAssigned = false; removeRestriction(record, field); } function installField(record, name) { const node = record.node; const own = Object.getOwnPropertyDescriptor(node, name); const native = inheritedDescriptor(node, name); const field = { name: name, own: own, native: native, hasAttribute: node.hasAttribute(name), attributeValue: node.getAttribute(name), intendedProperty: node[name], propertyAssigned: false, getter: null, setter: null }; record.fields[name] = field; removeRestriction(record, field); // 实例级别拦截这两个属性的重新赋值,不改变事件的点击行为。 if ((!own || own.configurable) && native && typeof native.set === 'function') { field.getter = function () { return null; }; field.setter = function (value) { writeNative(record, field, value); field.intendedProperty = readNative(record, field); field.propertyAssigned = true; writeNative(record, field, null); }; try { Object.defineProperty(node, name, { configurable: true, enumerable: own ? own.enumerable : true, get: field.getter, set: field.setter }); } catch (_) { field.getter = field.setter = null; } } } function installMethod(record, name) { const node = record.node; const own = Object.getOwnPropertyDescriptor(node, name); const original = node[name]; const wrapper = function (attributeName) { const result = original.apply(this, arguments); if (enabled && this === node) { const field = record.fields[String(attributeName).toLowerCase()]; if (field) rememberAttribute(record, field); } return result; }; if (!own || own.configurable) { try { Object.defineProperty(node, name, { configurable: true, writable: true, enumerable: own ? own.enumerable : false, value: wrapper }); record.methods[name] = { own: own, wrapper: wrapper }; } catch (_) { /* MutationObserver 仍会处理内联属性的重新设置。 */ } } } function watchBody() { if (!enabled || !doc.body || records.has(doc.body)) return; const node = doc.body; const record = { node: node, fields: {}, methods: {}, setAttribute: node.setAttribute, removeAttribute: node.removeAttribute }; records.set(node, record); EVENT_FIELDS.forEach(function (name) { installField(record, name); }); // 记录正文上对限制属性的最新修改意图。其他属性原样转交浏览器。 installMethod(record, 'setAttribute'); installMethod(record, 'removeAttribute'); } function restoreRecord(record) { const node = record.node; Object.keys(record.methods).forEach(function (name) { const method = record.methods[name]; if (node[name] !== method.wrapper) return; try { if (method.own) Object.defineProperty(node, name, method.own); else delete node[name]; } catch (_) { /* 若网页已接管字段,保留网页的新设置。 */ } }); EVENT_FIELDS.forEach(function (name) { const field = record.fields[name]; const current = Object.getOwnPropertyDescriptor(node, name); const ownsProperty = !field.getter || current && current.get === field.getter && current.set === field.setter; if (field.getter && !ownsProperty) return; if (field.getter && ownsProperty) { try { if (field.own) Object.defineProperty(node, name, field.own); else delete node[name]; } catch (_) { /* 保留不能恢复的字段。 */ } } // 当前仍有内联值时,说明网页刚改过它且观察回调尚未执行,保留该值。 if (node.hasAttribute(name)) return; if (field.hasAttribute) record.setAttribute.call(node, name, field.attributeValue); else record.removeAttribute.call(node, name); if (ownsProperty) { // 属性和事件函数可被分别设置,恢复实际保存的最后一个处理器。 writeNative(record, field, field.intendedProperty); // 先前实例上的自定义值不属于原生事件处理器,恢复其最新值。 if (field.own && 'value' in field.own && field.propertyAssigned) { try { node[name] = field.intendedProperty; } catch (_) { /* 只读属性保持原样。 */ } } } }); } function start() { style = doc.createElement('style'); style.setAttribute('data-yian-copy-style', ''); style.textContent = 'body,body *:not(input):not(button):not(select):not(option):not(textarea):not([data-yian-assistant]){-webkit-user-select:text!important;user-select:text!important;}'; (doc.head || doc.documentElement).appendChild(style); watchBody(); observer = new global.MutationObserver(function (mutations) { if (!enabled) return; watchBody(); mutations.forEach(function (mutation) { if (mutation.type !== 'attributes') return; const record = records.get(mutation.target); if (!record) return; const field = record.fields[mutation.attributeName]; // 自己移除的属性已是 null;有新的内联限制才记录并解除。 if (field && record.node.hasAttribute(field.name)) rememberAttribute(record, field); }); }); observer.observe(doc.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: EVENT_FIELDS }); } function stop() { if (observer) { observer.disconnect(); observer = null; } records.forEach(restoreRecord); records.clear(); if (style) { style.remove(); style = null; } } function setEnabled(value) { if (destroyed || enabled === Boolean(value)) return; enabled = Boolean(value); if (enabled) start(); else stop(); } function destroy() { if (destroyed) return; enabled = false; stop(); destroyed = true; } return { setEnabled: setEnabled, destroy: destroy }; } global.YianCopyController = { create: create }; })(typeof window !== 'undefined' ? window : globalThis); /* 独立题库助手面板:页面正文与面板样式互不影响。 */ (function (global) { 'use strict'; const ROLES = ['主操', '班长', '副操']; const PAGE_SIZE = 20; const TYPE_NAMES = { single: '单选', multiple: '多选', judgment: '判断', unknown: '未知' }; function plain(value) { return String(value == null ? '' : value); } function normalized(value) { return plain(value).replace(/\s+/g, ' ').trim(); } function el(doc, tag, className, value) { const node = doc.createElement(tag); if (className) node.className = className; if (value != null) node.textContent = value; return node; } function create(callbacks) { callbacks = callbacks || {}; const doc = global.document; const state = { autoAnswer: false, allowCopy: true, role: '', course: '', courses: [], bankTotal: 0, roleCounts: {}, storageMode: 'userscript', pageStatus: '等待识别题目页面', pageMode: '', canAnswer: false, canExport: false, metrics: {}, exportEnabled: false, expanded: false, loading: false }; let destroyed = false; let selectionText = ''; let currentOffset = 0; let resultTotal = 0; const host = doc.createElement('div'); host.id = 'yian-question-assistant'; host.setAttribute('data-yian-assistant', ''); host.style.cssText = 'all:initial!important;position:fixed!important;right:16px!important;bottom:16px!important;z-index:2147483646!important;display:block!important;max-width:calc(100vw - 24px)!important;font:13px/1.5 system-ui,"Microsoft YaHei",sans-serif!important;color:#17253d!important;'; const root = host.attachShadow({ mode: 'open' }); const style = doc.createElement('style'); style.textContent = ` :host{font-family:system-ui,-apple-system,"Microsoft YaHei",sans-serif;color:#17253d;font-size:13px;line-height:1.5;color-scheme:light;--accent:#225aca;--muted:#65728a;--line:#e4e9f1;--surface:#f5f7fb} *,*::before,*::after{box-sizing:border-box}button,input,select{font:inherit}button,input,select,label{-webkit-user-select:none;user-select:none}button{cursor:pointer}button:disabled,input:disabled,select:disabled{cursor:not-allowed;opacity:.5}button:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid #b8d1ff;outline-offset:2px}button{border:1px solid var(--line);background:white;color:#273953;border-radius:7px;padding:6px 10px}button:hover:enabled{background:#f2f6ff;border-color:#b6caf1}input[type=text],select{width:100%;min-width:0;border:1px solid #cfd8e7;border-radius:7px;background:#fff;color:#17253d;padding:8px}select{padding:6px 8px}input[type=checkbox],input[type=radio]{accent-color:var(--accent)} .panel{width:354px;max-width:calc(100vw - 24px);background:white;border:1px solid #dce3ee;border-radius:13px;box-shadow:0 8px 36px #14264528;overflow:hidden}.header{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;background:#172a49;color:white}.brand{font-size:15px;font-weight:700;letter-spacing:.2px}.toggle{border:0;background:#ffffff17;color:white;padding:3px 9px;border-radius:5px;min-width:30px}.toggle:hover:enabled{background:#ffffff2c;color:white;border-color:transparent}.body{padding:13px;max-height:calc(100vh - 102px);max-height:calc(100dvh - 102px);overflow:auto;overscroll-behavior:contain}.body[hidden]{display:none}.panel.collapsed{width:auto;min-width:140px}.collapsed .header{padding:10px 12px;gap:16px}.switches{display:grid;gap:9px;padding-bottom:12px;border-bottom:1px solid var(--line)}.switch-row{display:flex;align-items:center;justify-content:space-between;position:relative;gap:12px;cursor:pointer;font-weight:600}.switch{appearance:none;-webkit-appearance:none;width:35px;height:21px;margin:0;border:0!important;border-radius:20px;background:#b7c0ce;position:relative;transition:background .15s}.switch::after{content:"";position:absolute;width:15px;height:15px;top:3px;left:3px;border-radius:50%;background:white;box-shadow:0 1px 3px #0002;transition:transform .15s}.switch:checked{background:var(--accent)}.switch:checked::after{transform:translateX(14px)}.hint{font-size:12px;color:var(--muted);margin:6px 0 0;overflow-wrap:anywhere}.section{padding-top:12px}.section-title{font-size:13px;font-weight:700;margin:0 0 7px}.import-row{display:flex;align-items:center;gap:10px}.import-row button{white-space:nowrap}.bank-info{font-size:12px;color:var(--muted);overflow-wrap:anywhere}.storage{color:#97702e;margin:4px 0 0;font-size:11px}.roles{display:flex;gap:6px;flex-wrap:wrap}.role{display:flex;align-items:center;gap:4px;padding:6px 8px;border:1px solid var(--line);border-radius:7px;cursor:pointer;background:white;flex:1;justify-content:center;white-space:nowrap}.role.active{background:#edf3ff;border-color:#abc5fa;color:#194dab}.role input{margin:0}.role-count{font-size:10px;color:var(--muted)}.course-row{display:grid;grid-template-columns:36px 1fr;gap:7px;align-items:center;margin-top:8px}.search-row{display:flex;gap:6px;margin-top:9px}.search-row input{flex:1}.primary{background:var(--accent);border-color:var(--accent);color:white;white-space:nowrap}.primary:hover:enabled{background:#184baf;color:white;border-color:#184baf}.selection-search{margin-top:7px;font-size:12px;width:100%}.notice{padding:8px 9px;border-radius:7px;margin:10px 0 0;font-size:12px;overflow-wrap:anywhere;background:#edf3ff;color:#254e8b}.notice.warning{background:#fff5df;color:#8a5d11}.notice.error{background:#fff0ed;color:#a23a2c}.notice.success{background:#eaf7f0;color:#1e754c}.notice[hidden]{display:none}.results-title{font-size:12px;color:var(--muted);margin:11px 0 7px}.empty{background:var(--surface);border-radius:8px;padding:13px 10px;color:var(--muted);font-size:12px}.result{border:1px solid var(--line);border-radius:9px;padding:10px;margin-bottom:9px;-webkit-user-select:text;user-select:text}.question{font-weight:600;line-height:1.6;overflow-wrap:anywhere;margin:0 0 7px;white-space:pre-wrap}.type{display:inline-block;font-size:10px;font-weight:500;color:#556984;background:#eff3f8;padding:1px 5px;border-radius:4px;margin-right:5px;vertical-align:1px}.answer{font-weight:600;color:#1b714a;background:#edf8f1;border-radius:5px;padding:6px 7px;overflow-wrap:anywhere}.options{padding:0;margin:7px 0;list-style:none;font-size:12px;display:grid;gap:4px}.option{padding:3px 6px;border-radius:4px;overflow-wrap:anywhere;white-space:pre-wrap}.option.correct{color:#196442;background:#edf8f1;font-weight:600}.sources{font-size:11px;color:var(--muted);white-space:pre-wrap;overflow-wrap:anywhere;margin-top:5px}.conflict{color:#9d6012;background:#fff5df;border-radius:5px;font-size:11px;padding:6px;margin:6px 0}.copy-result{margin-top:8px;font-size:11px}.pagination{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:11px;color:var(--muted);margin:5px 0 0}.pagination button{font-size:11px;padding:4px 8px}.page-section{border-top:1px solid var(--line);margin-top:13px;padding-top:11px}.page-status{font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--muted)}.metrics{display:flex;flex-wrap:wrap;gap:4px 10px;font-size:11px;color:var(--muted);margin-top:6px}.unprocessed{font-size:11px;color:#8b621c;margin-top:7px}.unprocessed summary{cursor:pointer}.unprocessed-list{max-height:155px;overflow:auto;padding:7px;border-radius:6px;background:#fff7e8;margin-top:4px;display:grid;gap:5px;overflow-wrap:anywhere}.exports{display:flex;gap:6px;margin-top:9px}.exports button{flex:1;font-size:12px}.auto-export{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--muted);margin-top:8px}.auto-export input{margin:0}.busy{color:#83692c}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(max-width:420px){.body{padding:11px}.panel{width:350px}.role{padding:6px 5px}} `; style.textContent += '.answer-check{font-size:11px;color:#235c43;margin-top:7px}.answer-check summary{cursor:pointer}.answer-check-list{max-height:210px;overflow:auto;overscroll-behavior:contain;margin-top:5px;display:grid;gap:7px}.answer-check-item{border:1px solid #d9eade;border-radius:6px;background:#f0f8f3;padding:8px;overflow-wrap:anywhere;-webkit-user-select:text;user-select:text}.answer-check-question{font-weight:600;margin:0 0 4px;white-space:pre-wrap}.answer-check-course{color:var(--muted);margin:0 0 5px}.answer-check-label{font-weight:600;margin:0 0 3px}.answer-check-options{margin:0;padding:0;list-style:none;display:grid;gap:4px;white-space:pre-wrap}.answer-check-note{font-size:10px;color:var(--muted);margin:5px 0 0}'; root.appendChild(style); const visibilityStyle = doc.createElement('style'); visibilityStyle.textContent = '[hidden]{display:none!important}'; root.appendChild(visibilityStyle); const panel = el(doc, 'section', 'panel collapsed'); panel.setAttribute('aria-label', '谊安学助手 2.0'); const header = el(doc, 'div', 'header'); const brand = el(doc, 'span', 'brand', '题库助手'); const expand = el(doc, 'button', 'toggle', '展开'); expand.type = 'button'; expand.setAttribute('aria-label', '展开题库助手'); expand.setAttribute('aria-expanded', 'false'); header.append(brand, expand); const body = el(doc, 'div', 'body'); body.hidden = true; panel.append(header, body); root.appendChild(panel); function invoke(name) { if (destroyed || typeof callbacks[name] !== 'function') return; const args = Array.prototype.slice.call(arguments, 1); try { const result = callbacks[name].apply(null, args); if (result && typeof result.catch === 'function') result.catch(function (error) { setNotice(error && error.message || '操作失败,请重试。', 'error'); }); return result; } catch (error) { setNotice(error && error.message || '操作失败,请重试。', 'error'); } } function setNotice(message, level) { notice.textContent = plain(message); notice.className = 'notice ' + (['warning', 'error', 'success'].includes(level) ? level : 'info'); notice.hidden = !message; } function togglePanel() { state.expanded = !state.expanded; refresh(); } expand.addEventListener('click', togglePanel); brand.style.cursor = 'pointer'; brand.addEventListener('click', togglePanel); const switches = el(doc, 'div', 'switches'); function makeSwitch(labelText, callback, container) { const label = el(doc, 'label', 'switch-row'); const text = el(doc, 'span', '', labelText); const input = el(doc, 'input', 'switch'); input.type = 'checkbox'; input.setAttribute('role', 'switch'); input.setAttribute('aria-label', labelText); label.append(text, input); (container || switches).appendChild(label); input.addEventListener('change', callback); return { input: input, label: label }; } const autoSwitch = makeSwitch('自动选择答案', function () { state.autoAnswer = autoSwitch.input.checked; invoke('onAutoChange', state.autoAnswer); refresh(); }); const copySwitch = makeSwitch('允许复制文字', function () { state.allowCopy = copySwitch.input.checked; invoke('onCopyChange', state.allowCopy); refresh(); }); const answerHint = el(doc, 'p', 'hint'); switches.appendChild(answerHint); body.appendChild(switches); const bankSection = el(doc, 'div', 'section'); const importRow = el(doc, 'div', 'import-row'); const importButton = el(doc, 'button', '', '导入题库'); importButton.type = 'button'; const fileInput = el(doc, 'input'); fileInput.type = 'file'; fileInput.accept = '.zip,.csv,.json,application/zip,text/csv,application/json'; fileInput.multiple = true; fileInput.hidden = true; const bankInfo = el(doc, 'span', 'bank-info'); importRow.append(importButton, bankInfo, fileInput); const storageInfo = el(doc, 'p', 'storage'); bankSection.append(importRow, storageInfo); body.appendChild(bankSection); importButton.addEventListener('click', function () { fileInput.click(); }); fileInput.addEventListener('change', function () { const files = Array.from(fileInput.files || []); if (files.length) invoke('onImport', files); fileInput.value = ''; }); const searchSection = el(doc, 'div', 'section'); searchSection.appendChild(el(doc, 'h2', 'section-title', '搜索题库')); const roles = el(doc, 'div', 'roles'); roles.setAttribute('role', 'radiogroup'); roles.setAttribute('aria-label', '选择岗位'); const roleInputs = {}; const roleLabels = {}; const roleCounters = {}; ROLES.forEach(function (role) { const label = el(doc, 'label', 'role'); const input = el(doc, 'input'); input.type = 'radio'; input.name = 'yian-search-role'; input.value = role; input.setAttribute('aria-label', role); const counter = el(doc, 'span', 'role-count'); label.append(input, el(doc, 'span', '', role), counter); roles.appendChild(label); roleInputs[role] = input; roleLabels[role] = label; roleCounters[role] = counter; input.addEventListener('change', function () { if (!input.checked) return; const query = searchInput.value; state.role = role; state.course = ''; currentOffset = 0; refresh(); invoke('onRoleChange', role); if (normalized(query)) invoke('onSearch', query, { offset: 0 }); }); }); searchSection.appendChild(roles); const courseRow = el(doc, 'label', 'course-row'); const courseSelect = el(doc, 'select'); courseSelect.setAttribute('aria-label', '选择课程'); courseRow.append(el(doc, 'span', '', '课程'), courseSelect); courseSelect.addEventListener('change', function () { const query = searchInput.value; state.course = courseSelect.value; currentOffset = 0; invoke('onCourseChange', state.course); if (normalized(query)) invoke('onSearch', query, { offset: 0 }); }); searchSection.appendChild(courseRow); const searchRow = el(doc, 'div', 'search-row'); const searchInput = el(doc, 'input'); searchInput.type = 'text'; searchInput.placeholder = '输入题干、关键词或选项文字…'; searchInput.setAttribute('aria-label', '搜索题目'); const searchButton = el(doc, 'button', 'primary', '搜索'); searchButton.type = 'button'; searchRow.append(searchInput, searchButton); searchSection.appendChild(searchRow); const selectionButton = el(doc, 'button', 'selection-search', '用选中文字搜索'); selectionButton.type = 'button'; searchSection.appendChild(selectionButton); const notice = el(doc, 'div', 'notice'); notice.hidden = true; notice.setAttribute('role', 'status'); notice.setAttribute('aria-live', 'polite'); searchSection.appendChild(notice); const resultsTitle = el(doc, 'p', 'results-title'); const results = el(doc, 'div', 'results'); const pagination = el(doc, 'div', 'pagination'); const previous = el(doc, 'button', '', '上一页'); const pageLabel = el(doc, 'span'); const next = el(doc, 'button', '', '下一页'); previous.type = next.type = 'button'; pagination.append(previous, pageLabel, next); pagination.hidden = true; searchSection.append(resultsTitle, results, pagination); body.appendChild(searchSection); function search(offset) { if (!state.role) { setNotice('请先选择岗位:主操、班长或副操。', 'warning'); return; } if (!normalized(searchInput.value)) { setNotice('请输入题干、关键词或选项文字。', 'warning'); searchInput.focus(); return; } currentOffset = Math.max(0, Number(offset) || 0); invoke('onSearch', searchInput.value, { offset: currentOffset }); } searchButton.addEventListener('click', function () { search(0); }); searchInput.addEventListener('keydown', function (event) { if (event.key === 'Enter' && !event.isComposing) { event.preventDefault(); search(0); } }); previous.addEventListener('click', function () { search(Math.max(0, currentOffset - PAGE_SIZE)); }); next.addEventListener('click', function () { search(currentOffset + PAGE_SIZE); }); function rememberSelection() { const selected = global.getSelection && global.getSelection(); if (!selected || !selected.anchorNode || !selected.focusNode) return; if (host.contains(selected.anchorNode) || host.contains(selected.focusNode) || root.contains(selected.anchorNode) || root.contains(selected.focusNode)) return; const text = selected.toString().trim(); if (text) selectionText = text; } doc.addEventListener('selectionchange', rememberSelection); selectionButton.addEventListener('pointerdown', rememberSelection); selectionButton.addEventListener('click', function () { rememberSelection(); if (!state.role) { setNotice('请先选择岗位:主操、班长或副操。', 'warning'); return; } if (!selectionText) { setNotice('请先在网页正文选中题目文字。', 'warning'); return; } searchInput.value = selectionText; currentOffset = 0; if (typeof callbacks.onSelectionSearch === 'function') invoke('onSelectionSearch', selectionText); else search(0); }); const pageSection = el(doc, 'div', 'page-section'); const pageTitle = el(doc, 'h2', 'section-title', '当前页面'); pageSection.appendChild(pageTitle); const pageStatus = el(doc, 'div', 'page-status'); const metrics = el(doc, 'div', 'metrics'); const answerCheck = el(doc, 'details', 'answer-check'); const answerCheckSummary = el(doc, 'summary', '', '查看答案核对'); const answerCheckList = el(doc, 'div', 'answer-check-list'); answerCheck.append(answerCheckSummary, answerCheckList); answerCheck.hidden = true; const unprocessed = el(doc, 'details', 'unprocessed'); const unprocessedSummary = el(doc, 'summary', '', '查看未处理题目'); const unprocessedList = el(doc, 'div', 'unprocessed-list'); unprocessed.append(unprocessedSummary, unprocessedList); unprocessed.hidden = true; pageSection.append(pageStatus, metrics, answerCheck, unprocessed); body.appendChild(pageSection); const exportSection = el(doc, 'section', 'page-section export-section'); exportSection.setAttribute('aria-label', '题库导出设置'); const exportSwitch = makeSwitch('导出题库', function () { state.exportEnabled = exportSwitch.input.checked; refresh(); invoke('onExportEnabledChange', state.exportEnabled); }, exportSection); const exportContent = el(doc, 'div', 'export-content'); exportContent.id = 'yian-export-content'; exportContent.hidden = true; exportSwitch.input.setAttribute('aria-controls', exportContent.id); exportContent.appendChild(el(doc, 'p', 'hint', '仅在练习页面导出。开启后,练习题目和答案完整时自动导出 CSV;也可点击下方按钮重新导出。关闭后停止后续导出。')); const exportRow = el(doc, 'div', 'exports'); const exportButtons = []; ['csv', 'json', 'txt'].forEach(function (format) { const button = el(doc, 'button', '', '导出 ' + format.toUpperCase()); button.type = 'button'; button.addEventListener('click', function () { if (state.exportEnabled && state.canExport) invoke('onExport', format); }); exportRow.appendChild(button); exportButtons.push(button); }); exportContent.appendChild(exportRow); exportSection.appendChild(exportContent); body.appendChild(exportSection); function autoUnavailableReason() { if (!state.role) return '请选择岗位后再开启自动选择。'; if (!(Number(state.roleCounts && state.roleCounts[state.role]) > 0)) return '请导入包含' + state.role + '题目的题库。'; if (!state.canAnswer) return state.pageStatus || '当前页面未识别到支持的练习或考试题目。'; return ''; } autoSwitch.label.addEventListener('pointerdown', function () { const reason = autoUnavailableReason(); if (!state.autoAnswer && reason) setNotice(reason, 'warning'); }); const metricLabels = { selected: '已选择', pending: '待确认', missing: '未匹配', conflict: '冲突', locked: '已锁定', unsupported: '需人工核对', failed: '失败', skipped: '已跳过' }; const metricNodes = new Map(); const answerCheckNodes = new Map(); const unprocessedNodes = new Map(); function setText(node, value) { value = plain(value); if (node.textContent !== value) node.textContent = value; } // 每次进度更新复用相同题目的节点,保留列表滚动与 details 的展开状态。 function syncRows(container, entries, nodeCache, makeRow, updateRow) { const present = new Set(); let previousNode = null; entries.forEach(function (entry, index) { const key = index + '\u0000' + plain(entry.number) + '\u0000' + plain(entry.question); present.add(key); let saved = nodeCache.get(key); if (!saved) { saved = { node: makeRow(), signature: '' }; nodeCache.set(key, saved); } const signature = JSON.stringify([entry.number, entry.status, entry.reason, entry.question, entry.course, entry.correctTexts, entry.currentLabels]); if (signature !== saved.signature) { updateRow(saved.node, entry); saved.signature = signature; } const expectedNode = previousNode ? previousNode.nextSibling : container.firstChild; if (expectedNode !== saved.node) container.insertBefore(saved.node, expectedNode); previousNode = saved.node; }); nodeCache.forEach(function (saved, key) { if (!present.has(key)) { saved.node.remove(); nodeCache.delete(key); } }); } function refreshMetrics() { const progress = state.metrics || {}; Object.keys(metricLabels).forEach(function (key) { let node = metricNodes.get(key); if (progress[key] != null && !node) { node = el(doc, 'span'); metricNodes.set(key, node); metrics.appendChild(node); } if (!node) return; node.hidden = progress[key] == null; if (!node.hidden) setText(node, metricLabels[key] + ' ' + Number(progress[key] || 0)); }); const details = Array.isArray(progress.details) ? progress.details : []; const selected = details.filter(function (item) { return item.status === 'selected'; }); answerCheck.hidden = !selected.length; setText(answerCheckSummary, '查看答案核对(' + selected.length + ')'); syncRows(answerCheckList, selected, answerCheckNodes, function () { const row = el(doc, 'article', 'answer-check-item'); row.append(el(doc, 'p', 'answer-check-question'), el(doc, 'p', 'answer-check-course'), el(doc, 'p', 'answer-check-label', '题库正确答案全文 → 当前选项'), el(doc, 'ul', 'answer-check-options'), el(doc, 'p', 'answer-check-note')); return row; }, function (row, item) { setText(row.children[0], '第 ' + plain(item.number) + ' 题' + (item.question ? ' · ' + plain(item.question) : '')); const course = plain(item.course); setText(row.children[1], course ? '课程:' + course : '课程来源未记录'); const texts = Array.isArray(item.correctTexts) ? item.correctTexts : []; const labels = Array.isArray(item.currentLabels) ? item.currentLabels : []; const list = row.children[3]; texts.forEach(function (text, index) { let option = list.children[index]; if (!option) { option = el(doc, 'li'); list.appendChild(option); } setText(option, (labels[index] ? plain(labels[index]) + '. ' : '当前字母未记录 · ') + plain(text)); }); if (!texts.length) { let message = list.firstElementChild; if (!message) { message = el(doc, 'li'); list.appendChild(message); } setText(message, '题库答案文字未记录,请核对网页。'); } const count = Math.max(1, texts.length); while (list.children.length > count) list.lastElementChild.remove(); setText(row.children[4], item.reason || ''); row.children[4].hidden = !item.reason; }); const notProcessed = details.filter(function (item) { return item.status !== 'selected'; }); unprocessed.hidden = !notProcessed.length; setText(unprocessedSummary, '查看未处理题目(' + notProcessed.length + ')'); syncRows(unprocessedList, notProcessed, unprocessedNodes, function () { return el(doc, 'div'); }, function (row, item) { setText(row, '第 ' + plain(item.number) + ' 题 · ' + (metricLabels[item.status] || '已跳过') + ':' + (item.reason || '请人工核对。')); }); } let courseSignature = ''; function refresh() { if (destroyed) return; panel.classList.toggle('collapsed', !state.expanded); body.hidden = !state.expanded; brand.textContent = state.expanded ? '谊安学助手 2.0' : '题库助手'; expand.textContent = state.expanded ? '收起' : '展开'; expand.setAttribute('aria-label', state.expanded ? '收起题库助手' : '展开题库助手'); expand.setAttribute('aria-expanded', String(Boolean(state.expanded))); const unavailable = autoUnavailableReason(); autoSwitch.input.checked = Boolean(state.autoAnswer); autoSwitch.input.disabled = !state.autoAnswer && (Boolean(unavailable) || Boolean(state.loading)); autoSwitch.label.title = unavailable; copySwitch.input.checked = Boolean(state.allowCopy); answerHint.textContent = unavailable || (state.autoAnswer ? '正在按题干和选项全文匹配答案;关闭即可停止后续操作。' : '支持练习和考试,按题干和选项全文匹配答案;交卷由你操作。'); bankInfo.textContent = state.loading ? '正在读取题库…' : '已加载 ' + Number(state.bankTotal || 0).toLocaleString('zh-CN') + ' 条' + (state.role ? ' · ' + state.role + ' ' + Number(state.roleCounts && state.roleCounts[state.role] || 0).toLocaleString('zh-CN') + ' 条' : ''); bankInfo.classList.toggle('busy', Boolean(state.loading)); importButton.disabled = Boolean(state.loading); storageInfo.textContent = state.storageMode === 'origin' ? '此模式仅在当前网站保存题库和设置。' : ''; storageInfo.hidden = !storageInfo.textContent; ROLES.forEach(function (role) { roleInputs[role].checked = state.role === role; roleLabels[role].classList.toggle('active', state.role === role); const count = Number(state.roleCounts && state.roleCounts[role] || 0); roleCounters[role].textContent = count ? '(' + count.toLocaleString('zh-CN') + ')' : ''; }); const availableCourses = (state.courses || []).map(function (course) { return typeof course === 'string' ? { value: course, label: course } : { value: plain(course.value == null ? (course.course || course.name) : course.value), label: plain(course.label || course.name || course.course || course.value) }; }); const signature = JSON.stringify(availableCourses); if (signature !== courseSignature) { courseSignature = signature; courseSelect.replaceChildren(); const all = el(doc, 'option', '', '全部课程'); all.value = ''; courseSelect.appendChild(all); availableCourses.forEach(function (course) { const option = el(doc, 'option', '', course.label); option.value = course.value; courseSelect.appendChild(option); }); } courseSelect.value = state.course || ''; courseSelect.disabled = !state.role || Boolean(state.loading); searchButton.disabled = Boolean(state.loading); selectionButton.disabled = Boolean(state.loading); setText(pageTitle, state.pageMode === 'test' ? '当前考试' : state.pageMode === 'exercise' ? '当前练习' : '当前页面'); setText(pageStatus, state.pageStatus || '等待识别题目页面'); refreshMetrics(); exportSwitch.input.checked = Boolean(state.exportEnabled); exportContent.hidden = !state.exportEnabled; exportButtons.forEach(function (button) { button.disabled = !state.exportEnabled || !state.canExport; }); previous.disabled = currentOffset === 0 || Boolean(state.loading); next.disabled = currentOffset + PAGE_SIZE >= resultTotal || Boolean(state.loading); } function optionsOf(item) { if (Array.isArray(item.options)) return item.options.map(function (option, index) { return typeof option === 'string' ? { label: String.fromCharCode(65 + index), text: option } : { label: plain(option.label || option.key || String.fromCharCode(65 + index)), text: plain(option.text == null ? option.value : option.text), correct: option.correct }; }); return Object.keys(item.options || {}).map(function (label) { return { label: label, text: plain(item.options[label]) }; }); } function sourcesOf(item) { const entries = Array.isArray(item.sources) && item.sources.length ? item.sources : [{ role: item.role || state.role, course: item.course || item.title || item.bankTitle || '', file: item.filename || '' }]; return Array.from(new Set(entries.map(function (source) { if (typeof source === 'string') return source; return [source.role || item.role, source.course || source.title || source.bankTitle || source.name, source.filename || source.file].filter(Boolean).join(' / '); }).filter(Boolean))); } function answerInfo(item, options) { const answer = plain(item.answer == null ? item.correctAnswer : item.answer); const letters = plain(item.answerLabels || item.correctLabels || answer).replace(/[\s,,、;;.。]/g, '').toUpperCase(); const texts = Array.isArray(item.answerTexts) ? item.answerTexts.map(normalized) : []; const correctOptions = options.filter(function (option) { return option.correct || /^[A-Z]+$/.test(letters) && letters.includes(option.label.toUpperCase()) || texts.includes(normalized(option.text)); }); const description = item.answerText || correctOptions.map(function (option) { return option.label + ' — ' + (option.text || '[图片选项]'); }).join(';') || texts.join(';'); return { answer: answer, description: plain(description), correctOptions: correctOptions }; } async function copyText(value) { const legacyClipboard = typeof GM_setClipboard === 'function' ? GM_setClipboard : global.GM_setClipboard; const modernGM = typeof GM !== 'undefined' ? GM : global.GM; if (typeof legacyClipboard === 'function') { await legacyClipboard(value, 'text'); return; } if (modernGM && typeof modernGM.setClipboard === 'function') { await modernGM.setClipboard(value, 'text'); return; } if (global.navigator.clipboard && typeof global.navigator.clipboard.writeText === 'function') { try { await global.navigator.clipboard.writeText(value); return; } catch (_) { /* 再尝试浏览器复制命令。 */ } } const textarea = el(doc, 'textarea'); textarea.value = value; textarea.style.cssText = 'position:fixed;left:0;top:0;opacity:0;width:1px;height:1px;'; root.appendChild(textarea); textarea.focus(); textarea.select(); let copied = false; try { copied = doc.execCommand('copy'); } finally { textarea.remove(); } if (!copied) throw new Error('浏览器未允许复制,请选中结果文字手动复制。'); } function renderSearch(result) { if (destroyed) return; result = result || {}; const items = (result.items || []).slice(0, PAGE_SIZE); resultTotal = Number(result.total == null ? items.length : result.total) || 0; currentOffset = Math.max(0, Number(result.offset == null ? currentOffset : result.offset) || 0); resultsTitle.textContent = state.role ? '找到 ' + resultTotal.toLocaleString('zh-CN') + ' 道题 · 当前岗位:' + state.role : '先选择岗位,再搜索题目'; results.replaceChildren(); if (!items.length) results.appendChild(el(doc, 'div', 'empty', state.role ? '当前岗位未找到题目,可调整关键词、课程或切换岗位。' : '选择主操、班长或副操,导入题库后即可查找答案。')); items.forEach(function (item) { const card = el(doc, 'article', 'result'); const title = el(doc, 'p', 'question'); const type = TYPE_NAMES[item.type] || item.type || '题目'; title.append(el(doc, 'span', 'type', type), doc.createTextNode(plain(item.question == null ? item.stem : item.question))); card.appendChild(title); const options = optionsOf(item); const info = answerInfo(item, options); card.appendChild(el(doc, 'div', 'answer', '正确答案:' + (info.answer || '见下方选项') + (info.description ? ' · ' + info.description : ''))); const list = el(doc, 'ul', 'options'); options.forEach(function (option) { const correct = info.correctOptions.includes(option); const row = el(doc, 'li', 'option' + (correct ? ' correct' : ''), option.label + '. ' + (option.text || '[图片选项]') + (correct ? ' ✓' : '')); list.appendChild(row); }); card.appendChild(list); if (item.conflict || item.conflicts && item.conflicts.length || item.ambiguous) card.appendChild(el(doc, 'div', 'conflict', '此题存在不同答案,请核对课程来源;自动选择会跳过不明确的匹配。')); const sources = sourcesOf(item); card.appendChild(el(doc, 'div', 'sources', '来源:' + (sources.join('\n') || '未知'))); const copyButton = el(doc, 'button', 'copy-result', '复制题目与答案'); copyButton.type = 'button'; copyButton.addEventListener('click', async function () { const content = ['[' + type + '] ' + plain(item.question == null ? item.stem : item.question)].concat(options.map(function (option) { return option.label + '. ' + (option.text || '[图片选项]'); }), ['正确答案:' + info.answer, info.description ? '对应选项:' + info.description : '', '来源:' + sources.join(';')]).filter(Boolean).join('\n'); try { await copyText(content); setNotice('题目与答案已复制。', 'success'); } catch (error) { setNotice(error.message, 'error'); } }); card.appendChild(copyButton); results.appendChild(card); }); pagination.hidden = resultTotal <= PAGE_SIZE; pageLabel.textContent = resultTotal ? (currentOffset + 1) + '–' + Math.min(currentOffset + items.length, resultTotal) + ' / ' + resultTotal : '0 / 0'; refresh(); } function setState(partial) { if (destroyed) return; Object.assign(state, partial || {}); refresh(); } function destroy() { if (destroyed) return; destroyed = true; doc.removeEventListener('selectionchange', rememberSelection); host.remove(); } (doc.body || doc.documentElement).appendChild(host); renderSearch({ items: [], total: 0 }); refresh(); return { setState: setState, renderSearch: renderSearch, setNotice: setNotice, destroy: destroy, host: host, root: root }; } global.YianAssistantPanel = { create: create }; })(typeof window !== 'undefined' ? window : globalThis); /* YianAnswerRunner: 只在已明确识别的练习或考试页面串行点击真实选项。 */ (function (root) { 'use strict'; const CHECKED = 'examtest_content_list_des_checked'; const WAIT_STEP = 100; const WAIT_LIMIT = 1200; const CLICK_GAP = 150; const COUNT_KEYS = ['selected', 'pending', 'missing', 'conflict', 'locked', 'failed', 'unsupported']; const CONTEXT_KEYS = ['role', 'course', 'sourceUrl', 'routeKey', 'pageSignature', 'revision', 'exam', 'mode']; function normalize(value) { // 仅中和排版空白和不可见分隔符;不折叠大小写、数字、单位或标点。 return String(value == null ? '' : value).replace(/[\u200b-\u200d\u2060\uFEFF]/g, '').replace(/\s+/g, ' ').trim(); } function questionSignature(question) { // 答案、checked class 和展开解析不属于题目身份,判分后它们会正常变化。 return JSON.stringify([question.type, question.question, (question.questionImages || []), question.options.map(function (option) { return [option.label, option.text, option.images || []]; })]); } function bankSignature(bank) { return JSON.stringify([bank.title, bank.expectedTotal, bank.questions.map(questionSignature)]); } function isLocked(box, selectors, context) { if (box.querySelector('.pc_examtest_content_form_disabled')) return true; if (box.closest('[disabled],[aria-disabled="true"]')) return true; if (Array.from(box.querySelectorAll(selectors.option)).some(function (node) { return Boolean(node.closest('[disabled],[aria-disabled="true"]')); })) return true; // 考试正常作答时没有查看答案控件,单选/判断也允许改选。 // 已提交、到时或出现提交对话框由 getContext 将 exam 置 false,每一步都会复核。 if (context && context.exam === true) return false; const component = box.closest('[app-exam-question],app-exam-question'); const flags = [box, component].filter(Boolean); if (flags.some(function (element) { return element._showanswer === true || element.getAttribute('_showanswer') === 'true' || element.getAttribute('data-showanswer') === 'true'; })) return true; if (!box.querySelector('.pc_examtest_content_question_showanswer')) return true; // 原页面已判分时 option 的直接父容器由 child1 变为 child2;隐藏参考答案不表示锁定。 return Array.from(box.querySelectorAll(selectors.option)).some(function (node) { const parent = node.parentElement; return parent && parent.matches('.pc_examtest_content_list_child2,.pc_examtest_content_list_parent_child2,.parent_child2'); }); } function optionState(box, selectors, context) { const nodes = Array.from(box.querySelectorAll(selectors.option)); return { nodes: nodes, selected: nodes.map(function (node) { return node.classList.contains(CHECKED); }), locked: isLocked(box, selectors, context) }; } function sameSelection(actual, wanted) { return actual.length === wanted.length && actual.every(function (selected, index) { return selected === wanted[index]; }); } function create(options) { options = options || {}; if (typeof options.getContext !== 'function' || typeof options.match !== 'function') throw new TypeError('自动选择需要 getContext 和 match。'); const doc = root.document; const core = root.QuestionBankCore; if (!doc || !core || !core.selectors || typeof core.extract !== 'function') throw new Error('自动选择需要 QuestionBankCore 和当前网页。'); const selectors = core.selectors; let active = null; let scope = null; let records = new Map(); let total = 0; let destroyed = false; let generation = 0; function notifyState(state, reason) { if (typeof options.onState === 'function') options.onState(state, reason || ''); } function progress() { const counts = { selected: 0, pending: 0, missing: 0, conflict: 0, locked: 0, failed: 0, unsupported: 0, total: total }; const details = []; records.forEach(function (record) { if (COUNT_KEYS.includes(record.status)) counts[record.status]++; if (record.pending) counts.pending++; details.push({ number: record.number, question: record.question, course: record.course, pageCourse: record.pageCourse, sourceCourses: record.sourceCourses.slice(), status: record.status, reason: record.reason || '', correctTexts: record.correctTexts.slice(), correctLabels: record.correctLabels.slice(), currentLabels: record.currentLabels.slice(), currentTexts: record.currentTexts.slice(), currentAnswerText: record.currentAnswerText }); }); counts.details = details; return counts; } function notifyProgress() { const value = progress(); if (typeof options.onProgress === 'function') options.onProgress(value); return value; } function setRecord(item, status, reason, pending) { const nodes = Array.from(item.box.querySelectorAll(selectors.option)); const selected = item.question.options.filter(function (_, index) { return nodes[index] && nodes[index].classList.contains(CHECKED); }); const sourceCourses = (item.sourceCourses || []).slice(); records.set(item.key, { index: item.index, number: item.question.number, question: item.question.question, course: sourceCourses.length ? sourceCourses.join('、') : item.course, pageCourse: item.course, sourceCourses: sourceCourses, status: status, reason: reason || '', pending: Boolean(pending), correctTexts: (item.correctTexts || []).slice(), correctLabels: (item.correctLabels || []).slice(), currentLabels: selected.map(function (option) { return option.label; }), currentTexts: selected.map(function (option) { return option.text; }), currentAnswerText: selected.map(function (option) { return option.label + '. ' + option.text; }).join(';') }); notifyProgress(); } function captureContext(context) { const captured = { root: context.root, pageURL: String(root.location && root.location.href || '') }; CONTEXT_KEYS.forEach(function (key) { captured[key] = context[key]; }); return captured; } function sameContext(left, right) { return left && right && left.root === right.root && left.pageURL === right.pageURL && CONTEXT_KEYS.every(function (key) { return left[key] === right[key]; }); } function initialReason(context) { if (!context || context.ready !== true) return '等待答题页面就绪。'; if (context.local === true || root.location && root.location.protocol === 'file:') return '保存的静态网页可搜索和导出,不能记录自动选择。'; if (context.practice !== true && context.exam !== true) return '当前页面未识别为可作答的练习或考试,自动选择已停止。'; if (!context.role) return '请先选择主操、班长或副操。'; if (!context.root || !context.root.isConnected || context.root.ownerDocument !== doc) return '答题内容已切换,等待新页面。'; return ''; } function cancel(run) { if (!run || run.cancelled) return; run.cancelled = true; run.waiters.forEach(function (finish) { finish(false); }); run.waiters.clear(); } function endActive(state, reason) { const old = active; generation++; active = null; cancel(old); if (!destroyed) notifyState(state, reason); return progress(); } function pause(reason) { return endActive('paused', reason || '已暂停自动选择。'); } function stop(reason) { return endActive('stopped', reason || '自动选择已关闭。'); } function wait(run, milliseconds) { if (run.cancelled || active !== run) return Promise.resolve(false); return new Promise(function (resolve) { let timer; let settled = false; const finish = function (ok) { if (settled) return; settled = true; root.clearTimeout(timer); run.waiters.delete(finish); resolve(ok && !run.cancelled && active === run); }; run.waiters.add(finish); timer = root.setTimeout(function () { finish(true); }, milliseconds); }); } function contextReason(run) { if (destroyed || run.cancelled || active !== run || run.id !== generation) return '自动选择已取消。'; const context = options.getContext(); const reason = initialReason(context); if (reason) return reason; if (!sameContext(run.context, captureContext(context))) return '页面、岗位或题库已切换,已停止本轮自动选择。'; return ''; } function extractItems(context) { const boxes = Array.from(doc.querySelectorAll(selectors.question)); const bank = core.extract(doc); if (bank.questions.length !== boxes.length) throw new Error('题目与选项尚未同步完成。'); const items = boxes.map(function (box, index) { const question = bank.questions[index]; const signature = questionSignature(question); return { box: box, index: index, question: question, course: context.course || '', signature: signature, key: index + '\u0000' + signature }; }).filter(function (item) { return context.root === item.box || context.root.contains(item.box); }); return { items: items, signature: bankSignature(bank) }; } function readCurrent(run, item) { const reason = contextReason(run); if (reason) return { reason: reason, cancelled: true }; if (!item.box.isConnected || !run.context.root.contains(item.box) && run.context.root !== item.box) return { reason: '题目节点已更新,已停止本轮自动选择。', cancelled: true }; const boxes = Array.from(doc.querySelectorAll(selectors.question)); if (boxes[item.index] !== item.box) return { reason: '题目顺序已变化,已停止本轮自动选择。', cancelled: true }; const bank = core.extract(doc); // 父面板的 MutationObserver 尚未运行时,也直接阻断标题或其他题目的变化。 if (bankSignature(bank) !== run.bankSignature) return { reason: '题目或课程已更新,已停止本轮自动选择。', cancelled: true }; const current = bank.questions[item.index]; if (!current || questionSignature(current) !== item.signature) return { reason: '题干或选项已变化,已停止本轮自动选择。', cancelled: true }; const state = optionState(item.box, selectors, run.context); if (state.nodes.length !== current.options.length) return { reason: '选项尚未完整加载。' }; return { question: current, state: state }; } function clickable(node, item) { if (!node || !node.isConnected || !item.box.contains(node) || node.ownerDocument !== doc) return false; if (node.closest('[hidden],[inert]') || node.getAttribute('aria-disabled') === 'true' || node.hasAttribute('disabled')) return false; const style = root.getComputedStyle(node); return node.getClientRects().length > 0 && style.display !== 'none' && style.visibility !== 'hidden' && style.visibility !== 'collapse' && style.pointerEvents !== 'none'; } function abortForChange(run, reason) { if (active === run && !run.cancelled) stop(reason); return false; } async function clickOption(run, item, optionIndex, wantedChecked) { const before = readCurrent(run, item); if (before.cancelled) return { cancelled: true, reason: before.reason }; if (before.reason) return { reason: before.reason }; if (before.state.locked) return { locked: true, reason: '该题已判分或锁定,不能修改。' }; if (before.state.selected[optionIndex] === wantedChecked) return { ok: true }; const node = before.state.nodes[optionIndex]; if (!clickable(node, item)) return { reason: '选项不可点击,已跳过。' }; // 最后一次即时检查,避免异步匹配期间已经换路由或岗位。 const reason = contextReason(run); if (reason) return { cancelled: true, reason: reason }; node.click(); for (let elapsed = 0; elapsed <= WAIT_LIMIT; elapsed += WAIT_STEP) { const current = readCurrent(run, item); if (current.cancelled) return { cancelled: true, reason: current.reason }; if (current.reason) return { reason: current.reason }; if (current.state.selected[optionIndex] === wantedChecked) { if (!await wait(run, CLICK_GAP)) return { cancelled: true, reason: '自动选择已取消。' }; const settled = readCurrent(run, item); if (settled.cancelled) return { cancelled: true, reason: settled.reason }; if (settled.reason) return { reason: settled.reason }; if (settled.state.selected[optionIndex] !== wantedChecked) return { reason: '网页未保留选项状态,已停止修改该题。' }; return { ok: true }; } if (current.state.locked) return { locked: true, reason: '选项点击后题目锁定,但未确认目标选项,已停止修改该题。' }; if (elapsed < WAIT_LIMIT && !await wait(run, WAIT_STEP)) return { cancelled: true, reason: '自动选择已取消。' }; } return { reason: '网页在限定时间内未更新选项状态,已跳过。' }; } function resolveTargets(question, matched) { const labels = Array.isArray(matched.correctLabels) ? matched.correctLabels.map(normalize) : String(matched.correctLabels || '').split(/[\s,,、;;]+/).filter(Boolean); if (!labels.length || new Set(labels).size !== labels.length) return { reason: '正确选项为空或重复,已跳过。' }; const actualLabels = question.options.map(function (option) { return normalize(option.label); }); if (new Set(actualLabels).size !== actualLabels.length || labels.some(function (label) { return !actualLabels.includes(label); })) return { reason: '答案不能唯一对应网页选项,已跳过。' }; if (question.type !== '多选' && labels.length !== 1) return { reason: '单选或判断题匹配到多个答案,已跳过。' }; if (!Array.isArray(matched.correctTexts) || matched.correctTexts.length !== labels.length) return { reason: '题库缺少完整正确答案文字,不能仅凭答案字母自动选择。' }; const expectedTexts = matched.correctTexts.map(normalize); if (expectedTexts.some(function (text) { return !text; }) || new Set(expectedTexts).size !== expectedTexts.length) return { reason: '正确答案文字为空或重复,无法唯一核验。' }; const selected = question.options.filter(function (option) { return labels.includes(normalize(option.label)); }); const actualTexts = selected.map(function (option) { return normalize(option.text); }); if (JSON.stringify(expectedTexts.slice().sort()) !== JSON.stringify(actualTexts.slice().sort())) return { reason: '题库正确答案全文与网页选项不一致,已跳过。' }; return { wanted: actualLabels.map(function (label) { return labels.includes(label); }), correctLabels: selected.map(function (option) { return option.label; }), // 原始题库文本按当页选项顺序存入证据,方便与 currentLabels 逐项核对。 correctTexts: actualTexts.map(function (text) { return matched.correctTexts[expectedTexts.indexOf(text)]; }) }; } async function processQuestion(run, item) { run.currentBox = item.box; let current = readCurrent(run, item); if (current.cancelled) return abortForChange(run, current.reason); if (current.reason) { setRecord(item, 'failed', current.reason); return true; } const previous = records.get(item.key); if (previous && previous.status !== 'selected' && !previous.manualInvalid) return true; if (!item.box.closest('[app-exam-question],app-exam-question')) { setRecord(item, 'unsupported', '题目缺少已适配的答题组件,已跳过。'); return true; } if (!['单选', '多选', '判断'].includes(current.question.type)) { setRecord(item, 'unsupported', '暂不支持该题型自动选择。'); return true; } if (current.state.locked && !previous) { setRecord(item, 'locked', '该题已判分或锁定,保留原作答。'); return true; } const matched = await options.match(current.question, options.getContext()); current = readCurrent(run, item); if (current.cancelled) return abortForChange(run, current.reason); if (current.reason) { setRecord(item, 'failed', current.reason); return true; } item.correctTexts = matched && Array.isArray(matched.correctTexts) ? matched.correctTexts.slice() : []; item.correctLabels = matched && Array.isArray(matched.correctLabels) ? matched.correctLabels.slice() : []; item.sourceCourses = Array.from(new Set((matched && Array.isArray(matched.records) ? matched.records : []).map(function (record) { return normalize(record.course); }).filter(Boolean))); if (current.state.locked && !previous) { setRecord(item, 'locked', '匹配期间该题已锁定,保留原作答。'); return true; } if (!matched || matched.status !== 'matched') { const status = matched && ['missing', 'conflict', 'unsupported'].includes(matched.status) ? matched.status : 'unsupported'; setRecord(item, status, matched && matched.reason || '未获得可自动选择的唯一答案。'); return true; } const target = resolveTargets(current.question, matched); if (target.reason) { setRecord(item, 'unsupported', target.reason); return true; } item.correctTexts = target.correctTexts; item.correctLabels = target.correctLabels; if (current.state.locked) { if (sameSelection(current.state.selected, target.wanted)) setRecord(item, 'selected', '已重新核验原作答与题库答案一致。'); else setRecord(item, 'locked', '该题已锁定且当前作答不同于题库答案,保留原作答。'); return true; } let changed = false; if (current.question.type === '多选') { // 每次按当前集合核对;先去掉错误选项,随后补选目标选项,不重复 toggle。 const planned = []; target.wanted.forEach(function (wanted, index) { if (!wanted && current.state.selected[index]) planned.push({ index: index, checked: false }); }); target.wanted.forEach(function (wanted, index) { if (wanted && !current.state.selected[index]) planned.push({ index: index, checked: true }); }); for (const step of planned) { const result = await clickOption(run, item, step.index, step.checked); if (result.cancelled) return abortForChange(run, result.reason); if (!result.ok) { setRecord(item, result.locked && !changed ? 'locked' : 'failed', result.reason); return true; } changed = true; } } else { const wantedIndex = target.wanted.indexOf(true); if (!sameSelection(current.state.selected, target.wanted)) { if (current.state.selected[wantedIndex]) { setRecord(item, 'failed', '单选题有多个选中状态,已跳过。'); return true; } const result = await clickOption(run, item, wantedIndex, true); if (result.cancelled) return abortForChange(run, result.reason); if (!result.ok) { setRecord(item, result.locked ? 'locked' : 'failed', result.reason); return true; } changed = true; } } current = readCurrent(run, item); if (current.cancelled) return abortForChange(run, current.reason); if (current.reason) { setRecord(item, 'failed', current.reason); return true; } if (!sameSelection(current.state.selected, target.wanted)) { setRecord(item, 'failed', '网页当前选中集合与答案不一致,已停止修改该题。'); return true; } const pending = run.context.exam !== true && current.question.type === '多选' && !current.state.locked; setRecord(item, 'selected', pending ? '已选择,待网页确认。' : changed ? '已选择匹配答案。' : '当前已选中匹配答案。', pending); return true; } async function execute(run, items) { try { for (const item of items) { if (!await processQuestion(run, item)) return progress(); if (run.cancelled || active !== run) return progress(); } if (active === run && !run.cancelled) { active = null; notifyState('complete', '本页自动选择已完成。'); } } catch (error) { if (active === run && !run.cancelled) { if (run.currentBox) { const item = items.find(function (candidate) { return candidate.box === run.currentBox; }); if (item) setRecord(item, 'failed', error && error.message || '读取或点击选项失败。'); } pause(error && error.message || '自动选择遇到错误,已暂停。'); } } finally { cancel(run); } return progress(); } function start() { if (destroyed) return Promise.resolve(progress()); if (active && !active.cancelled) return active.promise; const context = options.getContext(); const reason = initialReason(context); if (reason) { notifyState('idle', reason); return Promise.resolve(progress()); } const captured = captureContext(context); if (!sameContext(scope, captured)) { scope = captured; records = new Map(); total = 0; } let extracted; try { extracted = extractItems(context); } catch (error) { notifyState('idle', error.message || '等待题目加载。'); return Promise.resolve(progress()); } const items = extracted.items; total = items.length; const currentKeys = new Set(items.map(function (item) { return item.key; })); records.forEach(function (_, key) { if (!currentKeys.has(key)) records.delete(key); }); notifyProgress(); if (!items.length) { notifyState('idle', '当前没有可处理的答题题目。'); return Promise.resolve(progress()); } const run = { id: ++generation, context: captured, bankSignature: extracted.signature, cancelled: false, waiters: new Set(), currentBox: null, promise: null }; active = run; // 先写入 promise,再通知或执行,使同步 UI 回调中的 start 也不会并发运行。 run.promise = Promise.resolve().then(function () { if (active !== run || run.cancelled) return progress(); notifyState('running', '正在选择当前岗位题库中的匹配答案。'); return execute(run, items); }); return run.promise; } function manualClick(event) { if (!event.isTrusted || destroyed) return; const target = event.target && event.target.nodeType === 1 ? event.target : event.target && event.target.parentElement; if (!target) return; const run = active && !active.cancelled ? active : null; let currentScope = run && run.context; if (!currentScope && scope && records.size) { const context = options.getContext(); if (!initialReason(context) && sameContext(scope, captureContext(context))) currentScope = scope; } if (!currentScope) return; const submit = target.closest('.pc_examtest_commit,button[type="submit"],input[type="submit"]'); if (submit && currentScope.root.contains(submit)) { pause('检测到手动交卷,已暂停自动选择。'); return; } const box = target.closest(selectors.question); if (!box || currentScope.root !== box && !currentScope.root.contains(box)) return; let invalidated = false; if (target.closest(selectors.option)) { const index = Array.from(doc.querySelectorAll(selectors.question)).indexOf(box); records.forEach(function (record) { if (record.index !== index || record.status !== 'selected') return; // 捕获阶段网页 onclick 还未执行,不能把此前的 checked 集合当成改选后的证据。 record.status = 'unsupported'; record.reason = '手动作答后需重新核对'; record.pending = false; record.manualInvalid = true; record.correctTexts = []; record.correctLabels = []; record.currentLabels = []; record.currentTexts = []; record.currentAnswerText = ''; invalidated = true; }); } if (invalidated) notifyProgress(); if (run || invalidated) pause('检测到手动作答,已暂停自动选择;可重新开启继续。'); } doc.addEventListener('click', manualClick, true); function reset() { stop('自动选择状态已重置。'); records = new Map(); scope = null; total = 0; return notifyProgress(); } function destroy() { if (destroyed) return; stop('自动选择已卸载。'); destroyed = true; doc.removeEventListener('click', manualClick, true); records.clear(); } return { start: start, stop: stop, reset: reset, destroy: destroy, get running() { return Boolean(active && !active.cancelled); } }; } root.YianAnswerRunner = { version: '1.1.0', create: create }; })(typeof window !== 'undefined' ? window : globalThis); (function () { 'use strict'; if (window.YianQuestionBankExporter) return; const core = window.QuestionBankCore; const store = window.YianBankStore; const matcher = window.YianAnswerMatcher; const config = { autoAnswer: false, allowCopy: true, exportEnabled: false, role: '', course: '' }; let snapshot = { records: [], counts: {}, total: 0, revision: '', storage: 'origin' }; let loading = true, disposed = false, page = null, previous = null; let searchQuery = '', runKey = '', debounceTimer; const exported = new Set(); const settleMs = 1200; const copy = window.YianCopyController.create(); const emptyMetrics = () => ({ selected: 0, pending: 0, missing: 0, conflict: 0, locked: 0, failed: 0, unsupported: 0, total: 0 }); let metrics = emptyMetrics(); function visible(node) { if (!node || node.closest('[hidden],.hidden')) return false; const style = getComputedStyle(node); return node.getClientRects().length > 0 && style.display !== 'none' && style.visibility !== 'hidden'; } function routeInfo() { const original = location.protocol === 'file:' ? core.savedSourceURL(document) : ''; const url = new URL(original || location.href); let params = url.searchParams; if (url.hash.startsWith('#/')) params = new URL(url.hash.slice(1), url.origin).searchParams; const root = document.querySelector('app-examtest, .pc_examtest'); const mode = (params.get('flag') || '').toLowerCase(); const marker = root && Array.from(root.querySelectorAll('.pc_examtest_commit')) .some(node => node.textContent.trim() === '重新练习' && !node.classList.contains('hidden')); const desktop = root && root.querySelector('.pc_examtest_left') && root.querySelector('.pc_examtest_card') && (root.matches('.pc_examtest') || root.querySelector('.pc_examtest')); const commit = root && root.querySelector('.pc_examtest_commit'); const countdown = root && root.querySelector('.pc_examtest_countdown .time'); const expired = countdown && /^\s*0{1,2}:00:00\s*$/.test(countdown.textContent); const submitting = Array.from(document.querySelectorAll('[beingsubmitted],.beingsubmitting,.exam_submited_box')).some(visible); const submitted = !!(root && root.querySelector('#examtestDisabled,.pc_examtest_commitdefult')) || submitting; // app-dialog-box 的 modal 模板会挂到全局;不能只检查组件后代。 const confirming = Array.from(document.querySelectorAll('.dialog_box,app-dialog-box > *,.examine_box')).some(visible); const practice = !!root && (mode ? mode === 'exercise' : !!marker); const exam = !!desktop && mode === 'test' && !!commit && visible(commit) && commit.textContent.trim() === '交卷' && !submitted && !expired && !confirming; const identity = new URL(location.href); identity.searchParams.delete('t'); return { root, practice, exam, answerSupported: practice || exam, expired: !!expired, submitted: !!submitted, confirming, local: location.protocol === 'file:', sourceUrl: url.href, key: identity.href, expectedTitle: mode === 'exercise' || mode === 'test' ? params.get('title') || '' : '', mode }; } function structureSignature(bank) { return JSON.stringify([bank.title, bank.expectedTotal, bank.questions.map(q => [q.number, q.type, q.question, q.options.map(option => [option.label, option.text, option.images])])]); } // 点击前现读上下文,阻断轮询尚未发现的路由、岗位、题目变化。 function getContext() { const route = routeInfo(); const bank = route.root ? core.extract(document, route.sourceUrl) : { title: '', questions: [] }; const signature = route.root ? structureSignature(bank) : ''; return { ready: !!page && page.ready && !loading && page.key === route.key && page.root === route.root && page.signature === signature, practice: route.practice, exam: route.exam, mode: route.mode, local: route.local, role: config.role, course: bank.title, sourceUrl: route.sourceUrl, routeKey: route.key, root: route.root, pageSignature: signature, revision: snapshot.revision, snapshot }; } const runner = window.YianAnswerRunner.create({ getContext, match: (question, context) => matcher.match(question, context, snapshot), onProgress: progress => { metrics = progress; panel.setState({ metrics }); }, onState: (state, reason) => { if (disposed) return; if (state === 'paused') { config.autoAnswer = false; runKey = ''; panel.setState({ autoAnswer: false }); } if (reason && (state === 'paused' || state === 'complete')) panel.setNotice(reason, state === 'paused' ? 'warning' : 'success'); } }); function courses() { return Array.from(new Set(snapshot.records.filter(record => record.role === config.role).map(record => record.course))).sort(); } function refreshPanel(extra) { panel.setState(Object.assign({ autoAnswer: config.autoAnswer, allowCopy: config.allowCopy, exportEnabled: config.exportEnabled, role: config.role, course: config.course, courses: courses(), bankTotal: snapshot.total, roleCounts: snapshot.counts, storageMode: snapshot.storage, loading, metrics }, extra || {})); } function performSearch(query = searchQuery, options = {}) { searchQuery = String(query || ''); const offset = options.offset || 0; const result = searchQuery.trim() ? matcher.search(searchQuery, { role: config.role, course: config.course, limit: 20, offset }, snapshot) : { items: [], total: 0 }; panel.renderSearch(Object.assign({}, result, { offset })); if (!config.role && !options.quiet) panel.setNotice('请先选择主操、班长或副操。', 'warning'); return result; } async function saveSetting(key, value) { try { await store.setSetting(key, value); } catch (error) { panel.setNotice('设置保存失败:' + error.message, 'error'); } } async function setRole(role) { if (!store.roles.includes(role)) throw new Error('请选择主操、班长或副操'); runner.stop('岗位已切换'); config.role = role; config.course = ''; runKey = ''; metrics = emptyMetrics(); refreshPanel(); performSearch(); tick(); await saveSetting('role', role); return role; } function setAutoAnswer(enabled) { const context = getContext(); if (enabled && (!context.ready || !(context.practice || context.exam) || context.local || !config.role || !snapshot.counts[config.role])) { panel.setNotice('请先导入所选岗位题库,并进入已加载完成的兼容练习或桌面考试页面。', 'warning'); refreshPanel(); return false; } config.autoAnswer = !!enabled; runKey = ''; if (!enabled) runner.stop('自动选择已关闭,已生效的选择保留。'); refreshPanel(); tick(); return config.autoAnswer; } function setCopy(enabled) { config.allowCopy = !!enabled; copy.setEnabled(config.allowCopy); refreshPanel(); saveSetting('allowCopy', config.allowCopy); } async function importFiles(files) { runner.stop('正在更新题库'); loading = true; runKey = ''; refreshPanel(); panel.setNotice('正在解析题库,请稍候…'); try { snapshot = await store.importFiles(Array.from(files || []), { role: config.role }); metrics = emptyMetrics(); config.course = ''; panel.setNotice('题库导入成功:' + snapshot.total + ' 条记录。请选择岗位后搜索或自动选择。', 'success'); return snapshot; } catch (error) { panel.setNotice('导入失败:' + error.message + '。原有题库保留。', 'error'); throw error; } finally { loading = false; refreshPanel(); performSearch(searchQuery, { quiet: true }); tick(); } } function exportCurrent(format = 'csv') { if (!config.exportEnabled) throw new Error('请先开启“导出题库”开关'); const route = routeInfo(); if (!route.practice) throw new Error('请先进入支持的练习页面'); const bank = core.extract(document, route.sourceUrl); if (!bank.total) throw new Error('页面题目尚未加载'); core.download(bank, format); panel.setNotice((bank.complete ? '已发起下载:' : '已发起不完整题库下载:') + bank.title + ',' + bank.total + ' 题、' + bank.answered + ' 个答案。若没有文件,请点击导出按钮。', bank.complete ? 'success' : 'warning'); return bank; } function setExportEnabled(enabled) { const opening = !!enabled && !config.exportEnabled; config.exportEnabled = !!enabled; // 用户主动重新开启可再次下载;同一开启期间只自动导出一次相同题库。 if (opening) exported.clear(); refreshPanel(); tick(); return config.exportEnabled; } const panel = window.YianAssistantPanel.create({ onAutoChange: setAutoAnswer, onCopyChange: setCopy, onRoleChange: role => { setRole(role).catch(error => panel.setNotice(error.message, 'error')); }, onCourseChange: course => { config.course = course; performSearch(); }, onImport: files => { importFiles(files).catch(() => {}); }, onSearch: performSearch, onSelectionSearch: text => performSearch(text), onExport: format => { try { exportCurrent(format); } catch (error) { panel.setNotice(error.message, 'error'); } }, onExportEnabledChange: setExportEnabled }); function tick() { if (disposed) return; const route = routeInfo(); if (!route.root) { if (runner.running) runner.stop('当前页面未识别到练习题目'); if (page) previous = page; page = null; refreshPanel({ canAnswer: false, canExport: false, pageMode: '', pageStatus: '当前网页可搜索题库和复制文字;未识别到支持的题目。' }); return; } const bank = core.extract(document, route.sourceUrl), signature = structureSignature(bank), now = Date.now(); if (!page || page.key !== route.key || page.root !== route.root) { if (runner.running) runner.stop('页面已切换,旧任务已停止'); const last = page || previous; page = { key: route.key, root: route.root, signature, since: now, ready: false, waiting: !!last && last.key !== route.key && last.root === route.root && last.signature === signature }; if (route.mode === 'test') config.autoAnswer = false; runKey = ''; metrics = emptyMetrics(); refreshPanel({ expanded: true }); } if (page.signature !== signature) { if (runner.running) runner.stop('题目内容已更新'); if (route.mode === 'test') config.autoAnswer = false; page.signature = signature; page.since = now; page.waiting = false; runKey = ''; metrics = emptyMetrics(); } const countComplete = route.mode === 'test' ? bank.expectedTotal > 0 && bank.expectedTotal === bank.total : bank.expectedTotal === null || bank.expectedTotal === bank.total; const titleReady = !route.expectedTitle || route.expectedTitle.trim() === bank.title.trim(); page.ready = !!bank.total && countComplete && titleReady && !page.waiting && now - page.since >= settleMs; let status; if (route.mode === 'test' && !route.exam) status = route.expired ? '考试计时已结束,自动选择已停止。' : route.submitted ? '考试已提交或提交中,自动选择已停止。' : route.confirming ? '考试对话框已打开,自动选择已停止。' : '考试页面尚未确认可作答,等待完整桌面试卷加载。'; else if (!route.answerSupported) status = '已识别题目,但未确认支持的练习或桌面考试模式。'; else if (page.waiting || !titleReady) status = '等待本次练习的题目加载…'; else if (!bank.total || now - page.since < settleMs) status = '正在读取题目与隐藏答案…'; else if (!countComplete) status = bank.expectedTotal === null ? '等待考试答题卡总题数,确认完整试卷后才能自动选择。' : '已读取 ' + bank.total + ' / ' + bank.expectedTotal + ' 题,等待全部加载。'; else if (route.exam) status = bank.title + ':' + bank.total + ' 题。按所选岗位题库严格核对题干、选项和正确答案全文。' + (route.local ? ' 保存网页只用于核对,自动选择需要在线考试页面。' : '完成后请检查结果并自行交卷。'); else status = bank.title + ':' + bank.total + ' 题,' + (bank.missingAnswers.length ? '缺少 ' + bank.missingAnswers.length + ' 个网页答案;可用导入题库选择。' : '网页答案齐全。') + (route.local ? ' 保存网页支持搜索与导出;自动选择需要在线练习页面。' : ''); if (route.mode === 'test' && !route.exam) { config.autoAnswer = false; runKey = ''; } refreshPanel({ pageStatus: status, pageMode: route.mode === 'test' ? 'test' : route.practice ? 'exercise' : '', canExport: route.practice && !!bank.total, canAnswer: route.answerSupported && !route.local && page.ready && !loading && !!config.role && !!snapshot.counts[config.role] }); if (!route.answerSupported || !page.ready || loading) { if (runner.running) runner.stop('等待可作答页面准备完成'); return; } if (route.practice && config.exportEnabled && bank.complete) { const key = route.key + '|' + signature; if (!exported.has(key)) { try { exportCurrent('csv'); exported.add(key); } catch (error) { config.exportEnabled = false; refreshPanel(); panel.setNotice('自动下载失败:' + error.message, 'error'); } } } const nextRunKey = route.key + '|' + signature + '|' + config.role + '|' + snapshot.revision; if (config.autoAnswer && config.role && snapshot.counts[config.role] && runKey !== nextRunKey && !runner.running) { runKey = nextRunKey; runner.start().catch(error => { config.autoAnswer = false; refreshPanel(); panel.setNotice('自动选择失败:' + error.message, 'error'); }); } } const observer = new MutationObserver(records => { if (records.every(record => record.target === panel.host || panel.host.contains(record.target))) return; clearTimeout(debounceTimer); debounceTimer = setTimeout(tick, 200); }); observer.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['class'] }); const timer = setInterval(tick, 600); const onNavigation = () => { runner.stop('页面地址已变化'); runKey = ''; tick(); }; window.addEventListener('popstate', onNavigation); window.addEventListener('hashchange', onNavigation); function dispose() { disposed = true; observer.disconnect(); clearInterval(timer); clearTimeout(debounceTimer); window.removeEventListener('popstate', onNavigation); window.removeEventListener('hashchange', onNavigation); runner.destroy(); copy.destroy(); panel.destroy(); } const ready = (async () => { try { const values = await Promise.all([store.load(), store.getSetting('role', ''), store.getSetting('allowCopy', true)]); snapshot = values[0]; config.role = store.roles.includes(values[1]) ? values[1] : ''; config.allowCopy = values[2] !== false; } catch (error) { panel.setNotice('题库读取失败:' + error.message + '。可重新导入题库。', 'error'); } loading = false; copy.setEnabled(config.allowCopy); refreshPanel(); tick(); return snapshot; })(); const api = Object.freeze({ ready, importFiles, setRole, setAutoAnswer, setCopy, setExportEnabled, search: performSearch, getContext, getSnapshot: () => snapshot, getState: () => ({ ...config, metrics, pageReady: !!page && page.ready }), exportCurrent, collectCurrent: () => core.extract(document, routeInfo().sourceUrl), stop: dispose }); window.YianAssistant = api; window.YianQuestionBankExporter = api; refreshPanel(); tick(); })();