Files
yianxue-assistant/谊安学助手 2.0.user.js
T

4046 lines
368 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==UserScript==
// @name 谊安学助手 2.0
// @namespace yianxue-question-bank
// @version 2.1.0
// @description 谊安学助手:统一题库、六栏面板、视频任务与考试全文核验。
// @match http://10.119.67.7/clientcn/*
// @run-at document-idle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_setClipboard
// @grant GM_openInTab
// @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 () {
/* Shared URL boundary for userscript injection and same-page navigation. */
const YianSiteScope = (function () {
'use strict';
const origin = 'http://10.119.67.7';
const basePath = '/clientcn/';
function isAllowedURL(value) {
try {
const url = new URL(String(value));
return url.origin === origin && url.pathname.startsWith(basePath);
} catch (_) { return false; }
}
return Object.freeze({ origin, basePath, isAllowedURL });
})();
if (typeof window === 'undefined' && typeof module !== 'undefined' && module.exports) module.exports = YianSiteScope;
if (!YianSiteScope.isAllowedURL(location.href)) return;
(function () { const module = { exports: {} }; const exports = module.exports;
/*!
JSZip v3.10.1 - A JavaScript class for generating and reading zip files
<http://stuartk.com/jszip>
(c) 2009-2016 Stuart Knightley <stuart [at] stuartk.com>
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<h.length;e++)u(h[e]);return u}({1:[function(e,t,r){"use strict";var d=e("./utils"),c=e("./support"),p="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";r.encode=function(e){for(var t,r,n,i,s,a,o,h=[],u=0,l=e.length,f=l,c="string"!==d.getTypeOf(e);u<e.length;)f=l-u,n=c?(t=e[u++],r=u<l?e[u++]:0,u<l?e[u++]:0):(t=e.charCodeAt(u++),r=u<l?e.charCodeAt(u++):0,u<l?e.charCodeAt(u++):0),i=t>>2,s=(3&t)<<4|r>>4,a=1<f?(15&r)<<2|n>>6:64,o=2<f?63&n:64,h.push(p.charAt(i)+p.charAt(s)+p.charAt(a)+p.charAt(o));return h.join("")},r.decode=function(e){var t,r,n,i,s,a,o=0,h=0,u="data:";if(e.substr(0,u.length)===u)throw new Error("Invalid base64 input, it looks like a data url.");var l,f=3*(e=e.replace(/[^A-Za-z0-9+/=]/g,"")).length/4;if(e.charAt(e.length-1)===p.charAt(64)&&f--,e.charAt(e.length-2)===p.charAt(64)&&f--,f%1!=0)throw new Error("Invalid base64 input, bad content length.");for(l=c.uint8array?new Uint8Array(0|f):new Array(0|f);o<e.length;)t=p.indexOf(e.charAt(o++))<<2|(i=p.indexOf(e.charAt(o++)))>>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<s;a++)e=e>>>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<s;a++)e=e>>>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<t;r++)n+=String.fromCharCode(255&e),e>>>=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.dirRecords.length;t++)this.push({data:this.dirRecords[t],meta:{percent:100}});var r=this.bytesWritten-e,n=function(e,t,r,n,i){var s=I.transformTo("string",i(n));return R.CENTRAL_DIRECTORY_END+"\0\0\0\0"+A(e,2)+A(e,2)+A(t,4)+A(r,4)+A(s.length,2)+s}(this.dirRecords.length,r,e,this.zipComment,this.encodeFileName);this.push({data:n,meta:{percent:100}})},s.prototype.prepareNextSource=function(){this.previous=this._sources.shift(),this.openedSource(this.previous.streamInfo),this.isPaused?this.previous.pause():this.previous.resume()},s.prototype.registerPrevious=function(e){this._sources.push(e);var t=this;return e.on("data",function(e){t.processChunk(e)}),e.on("end",function(){t.closedSource(t.previous.streamInfo),t._sources.length?t.prepareNextSource():t.end()}),e.on("error",function(e){t.error(e)}),this},s.prototype.resume=function(){return!!i.prototype.resume.call(this)&&(!this.previous&&this._sources.length?(this.prepareNextSource(),!0):this.previous||this._sources.length||this.generatedError?void 0:(this.end(),!0))},s.prototype.error=function(e){var t=this._sources;if(!i.prototype.error.call(this,e))return!1;for(var r=0;r<t.length;r++)try{t[r].error(e)}catch(e){}return!0},s.prototype.lock=function(){i.prototype.lock.call(this);for(var e=this._sources,t=0;t<e.length;t++)e[t].lock()},t.exports=s},{"../crc32":4,"../signature":23,"../stream/GenericWorker":28,"../utf8":31,"../utils":32}],9:[function(e,t,r){"use strict";var u=e("../compressions"),n=e("./ZipFileWorker");r.generateWorker=function(e,a,t){var o=new n(a.streamFiles,t,a.platform,a.encodeFileName),h=0;try{e.forEach(function(e,t){h++;var r=function(e,t){var r=e||t,n=u[r];if(!n)throw new Error(r+" is not a valid compression method !");return n}(t.options.compression,a.compression),n=t.options.compressionOptions||a.compressionOptions||{},i=t.dir,s=t.date;t._compressWorker(r,n).withStreamInfo("file",{name:e,dir:i,date:s,comment:t.comment||"",unixPermissions:t.unixPermissions,dosPermissions:t.dosPermissions}).pipe(o)}),o.entriesCount=h}catch(e){o.error(e)}return o}},{"../compressions":3,"./ZipFileWorker":8}],10:[function(e,t,r){"use strict";function n(){if(!(this instanceof n))return new n;if(arguments.length)throw new Error("The constructor with parameters has been removed in JSZip 3.0, please check the upgrade guide.");this.files=Object.create(null),this.comment=null,this.root="",this.clone=function(){var e=new n;for(var t in this)"function"!=typeof this[t]&&(e[t]=this[t]);return e}}(n.prototype=e("./object")).loadAsync=e("./load"),n.support=e("./support"),n.defaults=e("./defaults"),n.version="3.10.1",n.loadAsync=function(e,t){return(new n).loadAsync(e,t)},n.external=e("./external"),t.exports=n},{"./defaults":5,"./external":6,"./load":11,"./object":15,"./support":30}],11:[function(e,t,r){"use strict";var u=e("./utils"),i=e("./external"),n=e("./utf8"),s=e("./zipEntries"),a=e("./stream/Crc32Probe"),l=e("./nodejsUtils");function f(n){return new i.Promise(function(e,t){var r=n.decompressed.getContentWorker().pipe(new a);r.on("error",function(e){t(e)}).on("end",function(){r.streamInfo.crc32!==n.decompressed.crc32?t(new Error("Corrupted zip : CRC32 mismatch")):e()}).resume()})}t.exports=function(e,o){var h=this;return o=u.extend(o||{},{base64:!1,checkCRC32:!1,optimizedBinaryString:!1,createFolders:!1,decodeFileName:n.utf8decode}),l.isNode&&l.isStream(e)?i.Promise.reject(new Error("JSZip can't accept a stream when loading a zip file.")):u.prepareContent("the loaded zip file",e,!0,o.optimizedBinaryString,o.base64).then(function(e){var t=new s(o);return t.load(e),t}).then(function(e){var t=[i.Promise.resolve(e)],r=e.files;if(o.checkCRC32)for(var n=0;n<r.length;n++)t.push(f(r[n]));return i.Promise.all(t)}).then(function(e){for(var t=e.shift(),r=t.files,n=0;n<r.length;n++){var i=r[n],s=i.fileNameStr,a=u.resolve(i.fileNameStr);h.file(a,i.decompressed,{binary:!0,optimizedBinaryString:!0,date:i.date,dir:i.dir,comment:i.fileCommentStr.length?i.fileCommentStr:null,unixPermissions:i.unixPermissions,dosPermissions:i.dosPermissions,createFolders:o.createFolders}),i.dir||(h.file(a).unsafeOriginalName=s)}return t.zipComment.length&&(h.comment=t.zipComment),h})}},{"./external":6,"./nodejsUtils":14,"./stream/Crc32Probe":25,"./utf8":31,"./utils":32,"./zipEntries":33}],12:[function(e,t,r){"use strict";var n=e("../utils"),i=e("../stream/GenericWorker");function s(e,t){i.call(this,"Nodejs stream input adapter for "+e),this._upstreamEnded=!1,this._bindStream(t)}n.inherits(s,i),s.prototype._bindStream=function(e){var t=this;(this._stream=e).pause(),e.on("data",function(e){t.push({data:e,meta:{percent:0}})}).on("error",function(e){t.isPaused?this.generatedError=e:t.error(e)}).on("end",function(){t.isPaused?t._upstreamEnded=!0:t.end()})},s.prototype.pause=function(){return!!i.prototype.pause.call(this)&&(this._stream.pause(),!0)},s.prototype.resume=function(){return!!i.prototype.resume.call(this)&&(this._upstreamEnded?this.end():this._stream.resume(),!0)},t.exports=s},{"../stream/GenericWorker":28,"../utils":32}],13:[function(e,t,r){"use strict";var i=e("readable-stream").Readable;function n(e,t,r){i.call(this,t),this._helper=e;var n=this;e.on("data",function(e,t){n.push(e)||n._helper.pause(),r&&r(t)}).on("error",function(e){n.emit("error",e)}).on("end",function(){n.push(null)})}e("../utils").inherits(n,i),n.prototype._read=function(){this._helper.resume()},t.exports=n},{"../utils":32,"readable-stream":16}],14:[function(e,t,r){"use strict";t.exports={isNode:"undefined"!=typeof Buffer,newBufferFrom:function(e,t){if(Buffer.from&&Buffer.from!==Uint8Array.from)return Buffer.from(e,t);if("number"==typeof e)throw new Error('The "data" argument must not be a number');return new Buffer(e,t)},allocBuffer:function(e){if(Buffer.alloc)return Buffer.alloc(e);var t=new Buffer(e);return t.fill(0),t},isBuffer:function(e){return Buffer.isBuffer(e)},isStream:function(e){return e&&"function"==typeof e.on&&"function"==typeof e.pause&&"function"==typeof e.resume}}},{}],15:[function(e,t,r){"use strict";function s(e,t,r){var n,i=u.getTypeOf(t),s=u.extend(r||{},f);s.date=s.date||new Date,null!==s.compression&&(s.compression=s.compression.toUpperCase()),"string"==typeof s.unixPermissions&&(s.unixPermissions=parseInt(s.unixPermissions,8)),s.unixPermissions&&16384&s.unixPermissions&&(s.dir=!0),s.dosPermissions&&16&s.dosPermissions&&(s.dir=!0),s.dir&&(e=g(e)),s.createFolders&&(n=_(e))&&b.call(this,n,!0);var a="string"===i&&!1===s.binary&&!1===s.base64;r&&void 0!==r.binary||(s.binary=!a),(t instanceof c&&0===t.uncompressedSize||s.dir||!t||0===t.length)&&(s.base64=!1,s.binary=!0,t="",s.compression="STORE",i="string");var o=null;o=t instanceof c||t instanceof l?t:p.isNode&&p.isStream(t)?new m(e,t):u.prepareContent(e,t,s.binary,s.optimizedBinaryString,s.base64);var h=new d(e,o,s);this.files[e]=h}var i=e("./utf8"),u=e("./utils"),l=e("./stream/GenericWorker"),a=e("./stream/StreamHelper"),f=e("./defaults"),c=e("./compressedObject"),d=e("./zipObject"),o=e("./generate"),p=e("./nodejsUtils"),m=e("./nodejs/NodejsStreamInputAdapter"),_=function(e){"/"===e.slice(-1)&&(e=e.substring(0,e.length-1));var t=e.lastIndexOf("/");return 0<t?e.substring(0,t):""},g=function(e){return"/"!==e.slice(-1)&&(e+="/"),e},b=function(e,t){return t=void 0!==t?t:f.createFolders,e=g(e),this.files[e]||s.call(this,e,null,{dir:!0,createFolders:t}),this.files[e]};function h(e){return"[object RegExp]"===Object.prototype.toString.call(e)}var n={load:function(){throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide.")},forEach:function(e){var t,r,n;for(t in this.files)n=this.files[t],(r=t.slice(this.root.length,t.length))&&t.slice(0,this.root.length)===this.root&&e(r,n)},filter:function(r){var n=[];return this.forEach(function(e,t){r(e,t)&&n.push(t)}),n},file:function(e,t,r){if(1!==arguments.length)return e=this.root+e,s.call(this,e,t,r),this;if(h(e)){var n=e;return this.filter(function(e,t){return!t.dir&&n.test(e)})}var i=this.files[this.root+e];return i&&!i.dir?i:null},folder:function(r){if(!r)return this;if(h(r))return this.filter(function(e,t){return t.dir&&r.test(e)});var e=this.root+r,t=b.call(this,e),n=this.clone();return n.root=t.name,n},remove:function(r){r=this.root+r;var e=this.files[r];if(e||("/"!==r.slice(-1)&&(r+="/"),e=this.files[r]),e&&!e.dir)delete this.files[r];else for(var t=this.filter(function(e,t){return t.name.slice(0,r.length)===r}),n=0;n<t.length;n++)delete this.files[t[n].name];return this},generate:function(){throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide.")},generateInternalStream:function(e){var t,r={};try{if((r=u.extend(e||{},{streamFiles:!1,compression:"STORE",compressionOptions:null,type:"",platform:"DOS",comment:null,mimeType:"application/zip",encodeFileName:i.utf8encode})).type=r.type.toLowerCase(),r.compression=r.compression.toUpperCase(),"binarystring"===r.type&&(r.type="string"),!r.type)throw new Error("No output type specified.");u.checkSupport(r.type),"darwin"!==r.platform&&"freebsd"!==r.platform&&"linux"!==r.platform&&"sunos"!==r.platform||(r.platform="UNIX"),"win32"===r.platform&&(r.platform="DOS");var n=r.comment||this.comment||"";t=o.generateWorker(this,r,n)}catch(e){(t=new l("error")).error(e)}return new a(t,r.type||"string",r.mimeType)},generateAsync:function(e,t){return this.generateInternalStream(e).accumulate(t)},generateNodeStream:function(e,t){return(e=e||{}).type||(e.type="nodebuffer"),this.generateInternalStream(e).toNodejsStream(t)}};t.exports=n},{"./compressedObject":2,"./defaults":5,"./generate":9,"./nodejs/NodejsStreamInputAdapter":12,"./nodejsUtils":14,"./stream/GenericWorker":28,"./stream/StreamHelper":29,"./utf8":31,"./utils":32,"./zipObject":35}],16:[function(e,t,r){"use strict";t.exports=e("stream")},{stream:void 0}],17:[function(e,t,r){"use strict";var n=e("./DataReader");function i(e){n.call(this,e);for(var t=0;t<this.data.length;t++)e[t]=255&e[t]}e("../utils").inherits(i,n),i.prototype.byteAt=function(e){return this.data[this.zero+e]},i.prototype.lastIndexOfSignature=function(e){for(var t=e.charCodeAt(0),r=e.charCodeAt(1),n=e.charCodeAt(2),i=e.charCodeAt(3),s=this.length-4;0<=s;--s)if(this.data[s]===t&&this.data[s+1]===r&&this.data[s+2]===n&&this.data[s+3]===i)return s-this.zero;return-1},i.prototype.readAndCheckSignature=function(e){var t=e.charCodeAt(0),r=e.charCodeAt(1),n=e.charCodeAt(2),i=e.charCodeAt(3),s=this.readData(4);return t===s[0]&&r===s[1]&&n===s[2]&&i===s[3]},i.prototype.readData=function(e){if(this.checkOffset(e),0===e)return[];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}],18:[function(e,t,r){"use strict";var n=e("../utils");function i(e){this.data=e,this.length=e.length,this.index=0,this.zero=0}i.prototype={checkOffset:function(e){this.checkIndex(this.index+e)},checkIndex:function(e){if(this.length<this.zero+e||e<0)throw new Error("End of data reached (data length = "+this.length+", asked index = "+e+"). Corrupted zip ?")},setIndex:function(e){this.checkIndex(e),this.index=e},skip:function(e){this.setIndex(this.index+e)},byteAt:function(){},readInt:function(e){var t,r=0;for(this.checkOffset(e),t=this.index+e-1;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<this._listeners[e].length;r++)this._listeners[e][r].call(this,t)},pipe:function(e){return e.registerPrevious(this)},registerPrevious:function(e){if(this.isLocked)throw new Error("The stream '"+this+"' has already been used.");this.streamInfo=e.streamInfo,this.mergeStreamInfo(),this.previous=e;var t=this;return e.on("data",function(e){t.processChunk(e)}),e.on("end",function(){t.end()}),e.on("error",function(e){t.error(e)}),this},pause:function(){return!this.isPaused&&!this.isFinished&&(this.isPaused=!0,this.previous&&this.previous.pause(),!0)},resume:function(){if(!this.isPaused||this.isFinished)return!1;var e=this.isPaused=!1;return this.generatedError&&(this.error(this.generatedError),e=!0),this.previous&&this.previous.resume(),!e},flush:function(){},processChunk:function(e){this.push(e)},withStreamInfo:function(e,t){return this.extraStreamInfo[e]=t,this.mergeStreamInfo(),this},mergeStreamInfo:function(){for(var e in this.extraStreamInfo)Object.prototype.hasOwnProperty.call(this.extraStreamInfo,e)&&(this.streamInfo[e]=this.extraStreamInfo[e])},lock:function(){if(this.isLocked)throw new Error("The stream '"+this+"' has already been used.");this.isLocked=!0,this.previous&&this.previous.lock()},toString:function(){var e="Worker "+this.name;return this.previous?this.previous+" -> "+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<t.length;r++)s+=t[r].length;switch(e){case"string":return t.join("");case"array":return Array.prototype.concat.apply([],t);case"uint8array":for(i=new Uint8Array(s),r=0;r<t.length;r++)i.set(t[r],n),n+=t[r].length;return i;case"nodebuffer":return Buffer.concat(t);default:throw new Error("concat : unsupported type '"+e+"'")}}(i,n),a);t(e)}catch(e){r(e)}n=[]}).resume()})}function f(e,t,r){var n=t;switch(t){case"blob":case"arraybuffer":n="uint8array";break;case"base64":n="string"}try{this._internalType=n,this._outputType=t,this._mimeType=r,h.checkSupport(n),this._worker=e.pipe(new i(n)),e.lock()}catch(e){this._worker=new s("error"),this._worker.error(e)}}f.prototype={accumulate:function(e){return l(this,e)},on:function(e,t){var r=this;return"data"===e?this._worker.on(e,function(e){t.call(r,e.data,e.meta)}):this._worker.on(e,function(){h.delay(t,arguments,r)}),this},resume:function(){return h.delay(this._worker.resume,[],this._worker),this},pause:function(){return this._worker.pause(),this},toNodejsStream:function(e){if(h.checkSupport("nodestream"),"nodebuffer"!==this._outputType)throw new Error(this._outputType+" is not supported by this method");return new o(this,{objectMode:"nodebuffer"!==this._outputType},e)}},t.exports=f},{"../base64":1,"../external":6,"../nodejs/NodejsStreamOutputAdapter":13,"../support":30,"../utils":32,"./ConvertWorker":24,"./GenericWorker":28}],30:[function(e,t,r){"use strict";if(r.base64=!0,r.array=!0,r.string=!0,r.arraybuffer="undefined"!=typeof ArrayBuffer&&"undefined"!=typeof Uint8Array,r.nodebuffer="undefined"!=typeof Buffer,r.uint8array="undefined"!=typeof Uint8Array,"undefined"==typeof ArrayBuffer)r.blob=!1;else{var n=new ArrayBuffer(0);try{r.blob=0===new Blob([n],{type:"application/zip"}).size}catch(e){try{var i=new(self.BlobBuilder||self.WebKitBlobBuilder||self.MozBlobBuilder||self.MSBlobBuilder);i.append(n),r.blob=0===i.getBlob("application/zip").size}catch(e){r.blob=!1}}}try{r.nodestream=!!e("readable-stream").Readable}catch(e){r.nodestream=!1}},{"readable-stream":16}],31:[function(e,t,s){"use strict";for(var o=e("./utils"),h=e("./support"),r=e("./nodejsUtils"),n=e("./stream/GenericWorker"),u=new Array(256),i=0;i<256;i++)u[i]=252<=i?6:248<=i?5:240<=i?4:224<=i?3:192<=i?2:1;u[254]=u[254]=1;function a(){n.call(this,"utf-8 decode"),this.leftOver=null}function l(){n.call(this,"utf-8 encode")}s.utf8encode=function(e){return h.nodebuffer?r.newBufferFrom(e,"utf-8"):function(e){var t,r,n,i,s,a=e.length,o=0;for(i=0;i<a;i++)55296==(64512&(r=e.charCodeAt(i)))&&i+1<a&&56320==(64512&(n=e.charCodeAt(i+1)))&&(r=65536+(r-55296<<10)+(n-56320),i++),o+=r<128?1:r<2048?2:r<65536?3:4;for(t=h.uint8array?new Uint8Array(o):new Array(o),i=s=0;s<o;i++)55296==(64512&(r=e.charCodeAt(i)))&&i+1<a&&56320==(64512&(n=e.charCodeAt(i+1)))&&(r=65536+(r-55296<<10)+(n-56320),i++),r<128?t[s++]=r:(r<2048?t[s++]=192|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<s;)if((n=e[t++])<128)a[r++]=n;else if(4<(i=u[n]))a[r++]=65533,t+=i-1;else{for(n&=2===i?31:3===i?15:7;1<i&&t<s;)n=n<<6|63&e[t++],i--;1<i?a[r++]=65533:n<65536?a[r++]=n:(n-=65536,a[r++]=55296|n>>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<e.length;++r)t[r]=255&e.charCodeAt(r);return t}e("setimmediate"),a.newBlob=function(t,r){a.checkSupport("blob");try{return new Blob([t],{type:r})}catch(e){try{var n=new(self.BlobBuilder||self.WebKitBlobBuilder||self.MozBlobBuilder||self.MSBlobBuilder);return n.append(t),n.getBlob(r)}catch(e){throw new Error("Bug : can't construct the Blob.")}}};var i={stringifyByChunk:function(e,t,r){var n=[],i=0,s=e.length;if(s<=r)return String.fromCharCode.apply(null,e);for(;i<s;)"array"===t||"nodebuffer"===t?n.push(String.fromCharCode.apply(null,e.slice(i,Math.min(i+r,s)))):n.push(String.fromCharCode.apply(null,e.subarray(i,Math.min(i+r,s)))),i+=r;return n.join("")},stringifyByChar:function(e){for(var t="",r=0;r<e.length;r++)t+=String.fromCharCode(e[r]);return t},applyCanBeUsed:{uint8array:function(){try{return o.uint8array&&1===String.fromCharCode.apply(null,new Uint8Array(1)).length}catch(e){return!1}}(),nodebuffer:function(){try{return o.nodebuffer&&1===String.fromCharCode.apply(null,r.allocBuffer(1)).length}catch(e){return!1}}()}};function s(e){var t=65536,r=a.getTypeOf(e),n=!0;if("uint8array"===r?n=i.applyCanBeUsed.uint8array:"nodebuffer"===r&&(n=i.applyCanBeUsed.nodebuffer),n)for(;1<t;)try{return i.stringifyByChunk(e,r,t)}catch(e){t=Math.floor(t/2)}return i.stringifyByChar(e)}function f(e,t){for(var r=0;r<e.length;r++)t[r]=e[r];return t}a.applyFromCharCode=s;var c={};c.string={string:n,array:function(e){return l(e,new Array(e.length))},arraybuffer:function(e){return c.string.uint8array(e).buffer},uint8array:function(e){return l(e,new Uint8Array(e.length))},nodebuffer:function(e){return l(e,r.allocBuffer(e.length))}},c.array={string:s,array:n,arraybuffer:function(e){return new Uint8Array(e).buffer},uint8array:function(e){return new Uint8Array(e)},nodebuffer:function(e){return r.newBufferFrom(e)}},c.arraybuffer={string:function(e){return s(new Uint8Array(e))},array:function(e){return f(new Uint8Array(e),new Array(e.byteLength))},arraybuffer:n,uint8array:function(e){return new Uint8Array(e)},nodebuffer:function(e){return r.newBufferFrom(new Uint8Array(e))}},c.uint8array={string:s,array:function(e){return f(e,new Array(e.length))},arraybuffer:function(e){return e.buffer},uint8array:n,nodebuffer:function(e){return r.newBufferFrom(e)}},c.nodebuffer={string:s,array:function(e){return f(e,new Array(e.length))},arraybuffer:function(e){return c.nodebuffer.uint8array(e).buffer},uint8array:function(e){return f(e,new Uint8Array(e.length))},nodebuffer:n},a.transformTo=function(e,t){if(t=t||"",!e)return t;a.checkSupport(e);var r=a.getTypeOf(t);return c[r][e](t)},a.resolve=function(e){for(var t=e.split("/"),r=[],n=0;n<t.length;n++){var i=t[n];"."===i||""===i&&0!==n&&n!==t.length-1||(".."===i?r.pop():r.push(i))}return r.join("/")},a.getTypeOf=function(e){return"string"==typeof e?"string":"[object Array]"===Object.prototype.toString.call(e)?"array":o.nodebuffer&&r.isBuffer(e)?"nodebuffer":o.uint8array&&e instanceof Uint8Array?"uint8array":o.arraybuffer&&e instanceof ArrayBuffer?"arraybuffer":void 0},a.checkSupport=function(e){if(!o[e.toLowerCase()])throw new Error(e+" is not supported by this platform")},a.MAX_VALUE_16BITS=65535,a.MAX_VALUE_32BITS=-1,a.pretty=function(e){var t,r,n="";for(r=0;r<(e||"").length;r++)n+="\\x"+((t=e.charCodeAt(r))<16?"0":"")+t.toString(16).toUpperCase();return n},a.delay=function(e,t,r){setImmediate(function(){e.apply(r||null,t||[])})},a.inherits=function(e,t){function r(){}r.prototype=t.prototype,e.prototype=new r},a.extend=function(){var e,t,r={};for(e=0;e<arguments.length;e++)for(t in arguments[e])Object.prototype.hasOwnProperty.call(arguments[e],t)&&void 0===r[t]&&(r[t]=arguments[e][t]);return r},a.prepareContent=function(r,e,n,i,s){return u.Promise.resolve(e).then(function(n){return o.blob&&(n instanceof Blob||-1!==["[object File]","[object Blob]"].indexOf(Object.prototype.toString.call(n)))&&"undefined"!=typeof FileReader?new u.Promise(function(t,r){var e=new FileReader;e.onload=function(e){t(e.target.result)},e.onerror=function(e){r(e.target.error)},e.readAsArrayBuffer(n)}):n}).then(function(e){var t=a.getTypeOf(e);return t?("arraybuffer"===t?e=a.transformTo("uint8array",e):"string"===t&&(s?e=h.decode(e):n&&!0!==i&&(e=function(e){return l(e,o.uint8array?new Uint8Array(e.length):new Array(e.length))}(e))),e):u.Promise.reject(new Error("Can't read the data of '"+r+"'. Is it in a supported JavaScript type (String, Blob, ArrayBuffer, etc) ?"))})}},{"./base64":1,"./external":6,"./nodejsUtils":14,"./support":30,setimmediate:54}],33:[function(e,t,r){"use strict";var n=e("./reader/readerFor"),i=e("./utils"),s=e("./signature"),a=e("./zipEntry"),o=e("./support");function h(e){this.files=[],this.loadOptions=e}h.prototype={checkSignature:function(e){if(!this.reader.readAndCheckSignature(e)){this.reader.index-=4;var t=this.reader.readString(4);throw new Error("Corrupted zip or bug: unexpected signature ("+i.pretty(t)+", expected "+i.pretty(e)+")")}},isSignature:function(e,t){var r=this.reader.index;this.reader.setIndex(e);var n=this.reader.readString(4)===t;return this.reader.setIndex(r),n},readBlockEndOfCentral:function(){this.diskNumber=this.reader.readInt(2),this.diskWithCentralDirStart=this.reader.readInt(2),this.centralDirRecordsOnThisDisk=this.reader.readInt(2),this.centralDirRecords=this.reader.readInt(2),this.centralDirSize=this.reader.readInt(4),this.centralDirOffset=this.reader.readInt(4),this.zipCommentLength=this.reader.readInt(2);var e=this.reader.readData(this.zipCommentLength),t=o.uint8array?"uint8array":"array",r=i.transformTo(t,e);this.zipComment=this.loadOptions.decodeFileName(r)},readBlockZip64EndOfCentral:function(){this.zip64EndOfCentralSize=this.reader.readInt(8),this.reader.skip(4),this.diskNumber=this.reader.readInt(4),this.diskWithCentralDirStart=this.reader.readInt(4),this.centralDirRecordsOnThisDisk=this.reader.readInt(8),this.centralDirRecords=this.reader.readInt(8),this.centralDirSize=this.reader.readInt(8),this.centralDirOffset=this.reader.readInt(8),this.zip64ExtensibleData={};for(var e,t,r,n=this.zip64EndOfCentralSize-44;0<n;)e=this.reader.readInt(2),t=this.reader.readInt(4),r=this.reader.readData(t),this.zip64ExtensibleData[e]={id:e,length:t,value:r}},readBlockZip64EndOfCentralLocator:function(){if(this.diskWithZip64CentralDirStart=this.reader.readInt(4),this.relativeOffsetEndOfZip64CentralDir=this.reader.readInt(8),this.disksCount=this.reader.readInt(4),1<this.disksCount)throw new Error("Multi-volumes zip are not supported")},readLocalFiles:function(){var e,t;for(e=0;e<this.files.length;e++)t=this.files[e],this.reader.setIndex(t.localHeaderOffset),this.checkSignature(s.LOCAL_FILE_HEADER),t.readLocalPart(this.reader),t.handleUTF8(),t.processAttributes()},readCentralDir:function(){var e;for(this.reader.setIndex(this.centralDirOffset);this.reader.readAndCheckSignature(s.CENTRAL_FILE_HEADER);)(e=new a({zip64:this.zip64},this.loadOptions)).readCentralPart(this.reader),this.files.push(e);if(this.centralDirRecords!==this.files.length&&0!==this.centralDirRecords&&0===this.files.length)throw new Error("Corrupted zip or bug: expected "+this.centralDirRecords+" records in central dir, got "+this.files.length)},readEndOfCentral:function(){var e=this.reader.lastIndexOfSignature(s.CENTRAL_DIRECTORY_END);if(e<0)throw!this.isSignature(0,s.LOCAL_FILE_HEADER)?new Error("Can't find end of central directory : is this a zip file ? If it is, see https://stuk.github.io/jszip/documentation/howto/read_zip.html"):new Error("Corrupted zip: can't find end of central directory");this.reader.setIndex(e);var t=e;if(this.checkSignature(s.CENTRAL_DIRECTORY_END),this.readBlockEndOfCentral(),this.diskNumber===i.MAX_VALUE_16BITS||this.diskWithCentralDirStart===i.MAX_VALUE_16BITS||this.centralDirRecordsOnThisDisk===i.MAX_VALUE_16BITS||this.centralDirRecords===i.MAX_VALUE_16BITS||this.centralDirSize===i.MAX_VALUE_32BITS||this.centralDirOffset===i.MAX_VALUE_32BITS){if(this.zip64=!0,(e=this.reader.lastIndexOfSignature(s.ZIP64_CENTRAL_DIRECTORY_LOCATOR))<0)throw new Error("Corrupted zip: can't find the ZIP64 end of central directory locator");if(this.reader.setIndex(e),this.checkSignature(s.ZIP64_CENTRAL_DIRECTORY_LOCATOR),this.readBlockZip64EndOfCentralLocator(),!this.isSignature(this.relativeOffsetEndOfZip64CentralDir,s.ZIP64_CENTRAL_DIRECTORY_END)&&(this.relativeOffsetEndOfZip64CentralDir=this.reader.lastIndexOfSignature(s.ZIP64_CENTRAL_DIRECTORY_END),this.relativeOffsetEndOfZip64CentralDir<0))throw new Error("Corrupted zip: can't find the ZIP64 end of central directory");this.reader.setIndex(this.relativeOffsetEndOfZip64CentralDir),this.checkSignature(s.ZIP64_CENTRAL_DIRECTORY_END),this.readBlockZip64EndOfCentral()}var r=this.centralDirOffset+this.centralDirSize;this.zip64&&(r+=20,r+=12+this.zip64EndOfCentralSize);var n=t-r;if(0<n)this.isSignature(t,s.CENTRAL_FILE_HEADER)||(this.reader.zero=n);else if(n<0)throw new Error("Corrupted zip: missing "+Math.abs(n)+" bytes.")},prepareReader:function(e){this.reader=n(e)},load:function(e){this.prepareReader(e),this.readEndOfCentral(),this.readCentralDir(),this.readLocalFiles()}},t.exports=h},{"./reader/readerFor":22,"./signature":23,"./support":30,"./utils":32,"./zipEntry":34}],34:[function(e,t,r){"use strict";var n=e("./reader/readerFor"),s=e("./utils"),i=e("./compressedObject"),a=e("./crc32"),o=e("./utf8"),h=e("./compressions"),u=e("./support");function l(e,t){this.options=e,this.loadOptions=t}l.prototype={isEncrypted:function(){return 1==(1&this.bitFlag)},useUTF8:function(){return 2048==(2048&this.bitFlag)},readLocalPart:function(e){var t,r;if(e.skip(22),this.fileNameLength=e.readInt(2),r=e.readInt(2),this.fileName=e.readData(this.fileNameLength),e.skip(r),-1===this.compressedSize||-1===this.uncompressedSize)throw new Error("Bug or corrupted zip : didn't get enough information from the central directory (compressedSize === -1 || uncompressedSize === -1)");if(null===(t=function(e){for(var t in h)if(Object.prototype.hasOwnProperty.call(h,t)&&h[t].magic===e)return h[t];return null}(this.compressionMethod)))throw new Error("Corrupted zip : compression "+s.pretty(this.compressionMethod)+" unknown (inner file : "+s.transformTo("string",this.fileName)+")");this.decompressed=new i(this.compressedSize,this.uncompressedSize,this.crc32,t,e.readData(this.compressedSize))},readCentralPart:function(e){this.versionMadeBy=e.readInt(2),e.skip(2),this.bitFlag=e.readInt(2),this.compressionMethod=e.readString(2),this.date=e.readDate(),this.crc32=e.readInt(4),this.compressedSize=e.readInt(4),this.uncompressedSize=e.readInt(4);var t=e.readInt(2);if(this.extraFieldsLength=e.readInt(2),this.fileCommentLength=e.readInt(2),this.diskNumberStart=e.readInt(2),this.internalFileAttributes=e.readInt(2),this.externalFileAttributes=e.readInt(4),this.localHeaderOffset=e.readInt(4),this.isEncrypted())throw new Error("Encrypted zip are not supported");e.skip(t),this.readExtraFields(e),this.parseZIP64ExtraField(e),this.fileComment=e.readData(this.fileCommentLength)},processAttributes:function(){this.unixPermissions=null,this.dosPermissions=null;var e=this.versionMadeBy>>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<i;)t=e.readInt(2),r=e.readInt(2),n=e.readData(r),this.extraFields[t]={id:t,length:r,value:n};e.setIndex(i)},handleUTF8:function(){var e=u.uint8array?"uint8array":"array";if(this.useUTF8())this.fileNameStr=o.utf8decode(this.fileName),this.fileCommentStr=o.utf8decode(this.fileComment);else{var t=this.findExtraFieldUnicodePath();if(null!==t)this.fileNameStr=t;else{var r=s.transformTo(e,this.fileName);this.fileNameStr=this.loadOptions.decodeFileName(r)}var n=this.findExtraFieldUnicodeComment();if(null!==n)this.fileCommentStr=n;else{var i=s.transformTo(e,this.fileComment);this.fileCommentStr=this.loadOptions.decodeFileName(i)}}},findExtraFieldUnicodePath:function(){var e=this.extraFields[28789];if(e){var t=n(e.value);return 1!==t.readInt(1)?null:a(this.fileName)!==t.readInt(4)?null:o.utf8decode(t.readData(e.length-5))}return null},findExtraFieldUnicodeComment:function(){var e=this.extraFields[25461];if(e){var t=n(e.value);return 1!==t.readInt(1)?null:a(this.fileComment)!==t.readInt(4)?null:o.utf8decode(t.readData(e.length-5))}return null}},t.exports=l},{"./compressedObject":2,"./compressions":3,"./crc32":4,"./reader/readerFor":22,"./support":30,"./utf8":31,"./utils":32}],35:[function(e,t,r){"use strict";function n(e,t,r){this.name=e,this.dir=r.dir,this.date=r.date,this.comment=r.comment,this.unixPermissions=r.unixPermissions,this.dosPermissions=r.dosPermissions,this._data=t,this._dataBinary=r.binary,this.options={compression:r.compression,compressionOptions:r.compressionOptions}}var s=e("./stream/StreamHelper"),i=e("./stream/DataWorker"),a=e("./utf8"),o=e("./compressedObject"),h=e("./stream/GenericWorker");n.prototype={internalStream:function(e){var t=null,r="string";try{if(!e)throw new Error("No output type specified.");var n="string"===(r=e.toLowerCase())||"text"===r;"binarystring"!==r&&"text"!==r||(r="string"),t=this._decompressWorker();var i=!this._dataBinary;i&&!n&&(t=t.pipe(new a.Utf8EncodeWorker)),!i&&n&&(t=t.pipe(new a.Utf8DecodeWorker))}catch(e){(t=new h("error")).error(e)}return new s(t,r,"")},async:function(e,t){return this.internalStream(e).accumulate(t)},nodeStream:function(e,t){return this.internalStream(e||"nodebuffer").toNodejsStream(t)},_compressWorker:function(e,t){if(this._data instanceof o&&this._data.compression.magic===e.magic)return this._data.getCompressedWorker();var r=this._decompressWorker();return this._dataBinary||(r=r.pipe(new a.Utf8EncodeWorker)),o.createWorkerFrom(r,e,t)},_decompressWorker:function(){return this._data instanceof o?this._data.getContentWorker():this._data instanceof h?this._data:new i(this._data)}};for(var u=["asText","asBinary","asNodeBuffer","asUint8Array","asArrayBuffer"],l=function(){throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide.")},f=0;f<u.length;f++)n.prototype[u[f]]=l;t.exports=n},{"./compressedObject":2,"./stream/DataWorker":27,"./stream/GenericWorker":28,"./stream/StreamHelper":29,"./utf8":31}],36:[function(e,l,t){(function(t){"use strict";var r,n,e=t.MutationObserver||t.WebKitMutationObserver;if(e){var i=0,s=new e(u),a=t.document.createTextNode("");s.observe(a,{characterData:!0}),r=function(){a.data=i=++i%2}}else if(t.setImmediate||void 0===t.MessageChannel)r="document"in t&&"onreadystatechange"in t.document.createElement("script")?function(){var e=t.document.createElement("script");e.onreadystatechange=function(){u(),e.onreadystatechange=null,e.parentNode.removeChild(e),e=null},t.document.documentElement.appendChild(e)}:function(){setTimeout(u,0)};else{var o=new t.MessageChannel;o.port1.onmessage=u,r=function(){o.port2.postMessage(0)}}var h=[];function u(){var e,t;n=!0;for(var r=h.length;r;){for(t=h,h=[],e=-1;++e<r;)t[e]();r=h.length}n=!1}l.exports=function(e){1!==h.push(e)||n||r()}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],37:[function(e,t,r){"use strict";var i=e("immediate");function u(){}var l={},s=["REJECTED"],a=["FULFILLED"],n=["PENDING"];function o(e){if("function"!=typeof e)throw new TypeError("resolver must be a function");this.state=n,this.queue=[],this.outcome=void 0,e!==u&&d(this,e)}function h(e,t,r){this.promise=e,"function"==typeof t&&(this.onFulfilled=t,this.callFulfilled=this.otherCallFulfilled),"function"==typeof r&&(this.onRejected=r,this.callRejected=this.otherCallRejected)}function f(t,r,n){i(function(){var e;try{e=r(n)}catch(e){return l.reject(t,e)}e===t?l.reject(t,new TypeError("Cannot resolve promise with itself")):l.resolve(t,e)})}function c(e){var t=e&&e.then;if(e&&("object"==typeof e||"function"==typeof e)&&"function"==typeof t)return function(){t.apply(e,arguments)}}function d(t,e){var r=!1;function n(e){r||(r=!0,l.reject(t,e))}function i(e){r||(r=!0,l.resolve(t,e))}var s=p(function(){e(i,n)});"error"===s.status&&n(s.value)}function p(e,t){var r={};try{r.value=e(t),r.status="success"}catch(e){r.status="error",r.value=e}return r}(t.exports=o).prototype.finally=function(t){if("function"!=typeof t)return this;var r=this.constructor;return this.then(function(e){return r.resolve(t()).then(function(){return e})},function(e){return r.resolve(t()).then(function(){throw e})})},o.prototype.catch=function(e){return this.then(null,e)},o.prototype.then=function(e,t){if("function"!=typeof e&&this.state===a||"function"!=typeof t&&this.state===s)return this;var r=new this.constructor(u);this.state!==n?f(r,this.state===a?e:t,this.outcome):this.queue.push(new h(r,e,t));return r},h.prototype.callFulfilled=function(e){l.resolve(this.promise,e)},h.prototype.otherCallFulfilled=function(e){f(this.promise,this.onFulfilled,e)},h.prototype.callRejected=function(e){l.reject(this.promise,e)},h.prototype.otherCallRejected=function(e){f(this.promise,this.onRejected,e)},l.resolve=function(e,t){var r=p(c,t);if("error"===r.status)return l.reject(e,r.value);var n=r.value;if(n)d(e,n);else{e.state=a,e.outcome=t;for(var i=-1,s=e.queue.length;++i<s;)e.queue[i].callFulfilled(t)}return e},l.reject=function(e,t){e.state=s,e.outcome=t;for(var r=-1,n=e.queue.length;++r<n;)e.queue[r].callRejected(t);return e},o.resolve=function(e){if(e instanceof this)return e;return l.resolve(new this(u),e)},o.reject=function(e){var t=new this(u);return l.reject(t,e)},o.all=function(e){var r=this;if("[object Array]"!==Object.prototype.toString.call(e))return this.reject(new TypeError("must be an array"));var n=e.length,i=!1;if(!n)return this.resolve([]);var s=new Array(n),a=0,t=-1,o=new this(u);for(;++t<n;)h(e[t],t);return o;function h(e,t){r.resolve(e).then(function(e){s[t]=e,++a!==n||i||(i=!0,l.resolve(o,s))},function(e){i||(i=!0,l.reject(o,e))})}},o.race=function(e){var t=this;if("[object Array]"!==Object.prototype.toString.call(e))return this.reject(new TypeError("must be an array"));var r=e.length,n=!1;if(!r)return this.resolve([]);var i=-1,s=new this(u);for(;++i<r;)a=e[i],t.resolve(a).then(function(e){n||(n=!0,l.resolve(s,e))},function(e){n||(n=!0,l.reject(s,e))});var a;return s}},{immediate:36}],38:[function(e,t,r){"use strict";var n={};(0,e("./lib/utils/common").assign)(n,e("./lib/deflate"),e("./lib/inflate"),e("./lib/zlib/constants")),t.exports=n},{"./lib/deflate":39,"./lib/inflate":40,"./lib/utils/common":41,"./lib/zlib/constants":44}],39:[function(e,t,r){"use strict";var a=e("./zlib/deflate"),o=e("./utils/common"),h=e("./utils/strings"),i=e("./zlib/messages"),s=e("./zlib/zstream"),u=Object.prototype.toString,l=0,f=-1,c=0,d=8;function p(e){if(!(this instanceof p))return new p(e);this.options=o.assign({level:f,method:d,chunkSize:16384,windowBits:15,memLevel:8,strategy:c,to:""},e||{});var t=this.options;t.raw&&0<t.windowBits?t.windowBits=-t.windowBits:t.gzip&&0<t.windowBits&&t.windowBits<16&&(t.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new s,this.strm.avail_out=0;var r=a.deflateInit2(this.strm,t.level,t.method,t.windowBits,t.memLevel,t.strategy);if(r!==l)throw new Error(i[r]);if(t.header&&a.deflateSetHeader(this.strm,t.header),t.dictionary){var n;if(n="string"==typeof t.dictionary?h.string2buf(t.dictionary):"[object ArrayBuffer]"===u.call(t.dictionary)?new Uint8Array(t.dictionary):t.dictionary,(r=a.deflateSetDictionary(this.strm,n))!==l)throw new Error(i[r]);this._dict_set=!0}}function n(e,t){var r=new p(t);if(r.push(e,!0),r.err)throw r.msg||i[r.err];return r.result}p.prototype.push=function(e,t){var r,n,i=this.strm,s=this.options.chunkSize;if(this.ended)return!1;n=t===~~t?t:!0===t?4:0,"string"==typeof e?i.input=h.string2buf(e):"[object ArrayBuffer]"===u.call(e)?i.input=new Uint8Array(e):i.input=e,i.next_in=0,i.avail_in=i.input.length;do{if(0===i.avail_out&&(i.output=new o.Buf8(s),i.next_out=0,i.avail_out=s),1!==(r=a.deflate(i,n))&&r!==l)return this.onEnd(r),!(this.ended=!0);0!==i.avail_out&&(0!==i.avail_in||4!==n&&2!==n)||("string"===this.options.to?this.onData(h.buf2binstring(o.shrinkBuf(i.output,i.next_out))):this.onData(o.shrinkBuf(i.output,i.next_out)))}while((0<i.avail_in||0===i.avail_out)&&1!==r);return 4===n?(r=a.deflateEnd(this.strm),this.onEnd(r),this.ended=!0,r===l):2!==n||(this.onEnd(l),!(i.avail_out=0))},p.prototype.onData=function(e){this.chunks.push(e)},p.prototype.onEnd=function(e){e===l&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=o.flattenChunks(this.chunks)),this.chunks=[],this.err=e,this.msg=this.strm.msg},r.Deflate=p,r.deflate=n,r.deflateRaw=function(e,t){return(t=t||{}).raw=!0,n(e,t)},r.gzip=function(e,t){return(t=t||{}).gzip=!0,n(e,t)}},{"./utils/common":41,"./utils/strings":42,"./zlib/deflate":46,"./zlib/messages":51,"./zlib/zstream":53}],40:[function(e,t,r){"use strict";var c=e("./zlib/inflate"),d=e("./utils/common"),p=e("./utils/strings"),m=e("./zlib/constants"),n=e("./zlib/messages"),i=e("./zlib/zstream"),s=e("./zlib/gzheader"),_=Object.prototype.toString;function a(e){if(!(this instanceof a))return new a(e);this.options=d.assign({chunkSize:16384,windowBits:0,to:""},e||{});var t=this.options;t.raw&&0<=t.windowBits&&t.windowBits<16&&(t.windowBits=-t.windowBits,0===t.windowBits&&(t.windowBits=-15)),!(0<=t.windowBits&&t.windowBits<16)||e&&e.windowBits||(t.windowBits+=32),15<t.windowBits&&t.windowBits<48&&0==(15&t.windowBits)&&(t.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new i,this.strm.avail_out=0;var r=c.inflateInit2(this.strm,t.windowBits);if(r!==m.Z_OK)throw new Error(n[r]);this.header=new s,c.inflateGetHeader(this.strm,this.header)}function o(e,t){var r=new a(t);if(r.push(e,!0),r.err)throw r.msg||n[r.err];return r.result}a.prototype.push=function(e,t){var r,n,i,s,a,o,h=this.strm,u=this.options.chunkSize,l=this.options.dictionary,f=!1;if(this.ended)return!1;n=t===~~t?t:!0===t?m.Z_FINISH:m.Z_NO_FLUSH,"string"==typeof e?h.input=p.binstring2buf(e):"[object ArrayBuffer]"===_.call(e)?h.input=new Uint8Array(e):h.input=e,h.next_in=0,h.avail_in=h.input.length;do{if(0===h.avail_out&&(h.output=new d.Buf8(u),h.next_out=0,h.avail_out=u),(r=c.inflate(h,m.Z_NO_FLUSH))===m.Z_NEED_DICT&&l&&(o="string"==typeof l?p.string2buf(l):"[object ArrayBuffer]"===_.call(l)?new Uint8Array(l):l,r=c.inflateSetDictionary(this.strm,o)),r===m.Z_BUF_ERROR&&!0===f&&(r=m.Z_OK,f=!1),r!==m.Z_STREAM_END&&r!==m.Z_OK)return this.onEnd(r),!(this.ended=!0);h.next_out&&(0!==h.avail_out&&r!==m.Z_STREAM_END&&(0!==h.avail_in||n!==m.Z_FINISH&&n!==m.Z_SYNC_FLUSH)||("string"===this.options.to?(i=p.utf8border(h.output,h.next_out),s=h.next_out-i,a=p.buf2string(h.output,i),h.next_out=s,h.avail_out=u-s,s&&d.arraySet(h.output,h.output,i,s,0),this.onData(a)):this.onData(d.shrinkBuf(h.output,h.next_out)))),0===h.avail_in&&0===h.avail_out&&(f=!0)}while((0<h.avail_in||0===h.avail_out)&&r!==m.Z_STREAM_END);return r===m.Z_STREAM_END&&(n=m.Z_FINISH),n===m.Z_FINISH?(r=c.inflateEnd(this.strm),this.onEnd(r),this.ended=!0,r===m.Z_OK):n!==m.Z_SYNC_FLUSH||(this.onEnd(m.Z_OK),!(h.avail_out=0))},a.prototype.onData=function(e){this.chunks.push(e)},a.prototype.onEnd=function(e){e===m.Z_OK&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=d.flattenChunks(this.chunks)),this.chunks=[],this.err=e,this.msg=this.strm.msg},r.Inflate=a,r.inflate=o,r.inflateRaw=function(e,t){return(t=t||{}).raw=!0,o(e,t)},r.ungzip=o},{"./utils/common":41,"./utils/strings":42,"./zlib/constants":44,"./zlib/gzheader":47,"./zlib/inflate":49,"./zlib/messages":51,"./zlib/zstream":53}],41:[function(e,t,r){"use strict";var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Int32Array;r.assign=function(e){for(var t=Array.prototype.slice.call(arguments,1);t.length;){var r=t.shift();if(r){if("object"!=typeof r)throw new TypeError(r+"must be non-object");for(var n in r)r.hasOwnProperty(n)&&(e[n]=r[n])}}return e},r.shrinkBuf=function(e,t){return e.length===t?e:e.subarray?e.subarray(0,t):(e.length=t,e)};var i={arraySet:function(e,t,r,n,i){if(t.subarray&&e.subarray)e.set(t.subarray(r,r+n),i);else for(var s=0;s<n;s++)e[i+s]=t[r+s]},flattenChunks:function(e){var t,r,n,i,s,a;for(t=n=0,r=e.length;t<r;t++)n+=e[t].length;for(a=new Uint8Array(n),t=i=0,r=e.length;t<r;t++)s=e[t],a.set(s,i),i+=s.length;return a}},s={arraySet:function(e,t,r,n,i){for(var s=0;s<n;s++)e[i+s]=t[r+s]},flattenChunks:function(e){return[].concat.apply([],e)}};r.setTyped=function(e){e?(r.Buf8=Uint8Array,r.Buf16=Uint16Array,r.Buf32=Int32Array,r.assign(r,i)):(r.Buf8=Array,r.Buf16=Array,r.Buf32=Array,r.assign(r,s))},r.setTyped(n)},{}],42:[function(e,t,r){"use strict";var h=e("./common"),i=!0,s=!0;try{String.fromCharCode.apply(null,[0])}catch(e){i=!1}try{String.fromCharCode.apply(null,new Uint8Array(1))}catch(e){s=!1}for(var u=new h.Buf8(256),n=0;n<256;n++)u[n]=252<=n?6:248<=n?5:240<=n?4:224<=n?3:192<=n?2:1;function l(e,t){if(t<65537&&(e.subarray&&s||!e.subarray&&i))return String.fromCharCode.apply(null,h.shrinkBuf(e,t));for(var r="",n=0;n<t;n++)r+=String.fromCharCode(e[n]);return r}u[254]=u[254]=1,r.string2buf=function(e){var t,r,n,i,s,a=e.length,o=0;for(i=0;i<a;i++)55296==(64512&(r=e.charCodeAt(i)))&&i+1<a&&56320==(64512&(n=e.charCodeAt(i+1)))&&(r=65536+(r-55296<<10)+(n-56320),i++),o+=r<128?1:r<2048?2:r<65536?3:4;for(t=new h.Buf8(o),i=s=0;s<o;i++)55296==(64512&(r=e.charCodeAt(i)))&&i+1<a&&56320==(64512&(n=e.charCodeAt(i+1)))&&(r=65536+(r-55296<<10)+(n-56320),i++),r<128?t[s++]=r:(r<2048?t[s++]=192|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},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<n;r++)t[r]=e.charCodeAt(r);return t},r.buf2string=function(e,t){var r,n,i,s,a=t||e.length,o=new Array(2*a);for(r=n=0;r<a;)if((i=e[r++])<128)o[n++]=i;else if(4<(s=u[i]))o[n++]=65533,r+=s-1;else{for(i&=2===s?31:3===s?15:7;1<s&&r<a;)i=i<<6|63&e[r++],s--;1<s?o[n++]=65533:i<65536?o[n++]=i:(i-=65536,o[n++]=55296|i>>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<r?2e3:r;s=s+(i=i+t[n++]|0)|0,--a;);i%=65521,s%=65521}return i|s<<16|0}},{}],44:[function(e,t,r){"use strict";t.exports={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8}},{}],45:[function(e,t,r){"use strict";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,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>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)-(4<e?9:0)}function D(e){for(var t=e.length;0<=--t;)e[t]=0}function F(e){var t=e.state,r=t.pending;r>e.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]&&s<c);if(n=S-(c-s),s=c-S,a<n){if(e.match_start=t,o<=(a=n))break;d=u[s+a-1],p=u[s+a]}}}while((t=f[t&l])>h&&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<l&&(l=u),r=0===l?0:(a.avail_in-=l,c.arraySet(o,a.input,a.next_in,l,h),1===a.state.wrap?a.adler=d(a.adler,o,l,h):2===a.state.wrap&&(a.adler=p(a.adler,o,l,h)),a.next_in+=l,a.total_in+=l,l),e.lookahead+=r,e.lookahead+e.insert>=x)for(s=e.strstart-e.insert,e.ins_h=e.window[s],e.ins_h=(e.ins_h<<e.hash_shift^e.window[s+1])&e.hash_mask;e.insert&&(e.ins_h=(e.ins_h<<e.hash_shift^e.window[s+x-1])&e.hash_mask,e.prev[s&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=s,s++,e.insert--,!(e.lookahead+e.insert<x)););}while(e.lookahead<z&&0!==e.strm.avail_in)}function Z(e,t){for(var r,n;;){if(e.lookahead<z){if(j(e),e.lookahead<z&&t===l)return A;if(0===e.lookahead)break}if(r=0,e.lookahead>=x&&(e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+x-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart),0!==r&&e.strstart-r<=e.w_size-z&&(e.match_length=L(e,r)),e.match_length>=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<<e.hash_shift^e.window[e.strstart+x-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart,0!=--e.match_length;);e.strstart++}else e.strstart+=e.match_length,e.match_length=0,e.ins_h=e.window[e.strstart],e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+1])&e.hash_mask;else n=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++;if(n&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=e.strstart<x-1?e.strstart:x-1,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}function W(e,t){for(var r,n,i;;){if(e.lookahead<z){if(j(e),e.lookahead<z&&t===l)return A;if(0===e.lookahead)break}if(r=0,e.lookahead>=x&&(e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+x-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart),e.prev_length=e.match_length,e.prev_match=e.match_start,e.match_length=x-1,0!==r&&e.prev_length<e.max_lazy_match&&e.strstart-r<=e.w_size-z&&(e.match_length=L(e,r),e.match_length<=5&&(1===e.strategy||e.match_length===x&&4096<e.strstart-e.match_start)&&(e.match_length=x-1)),e.prev_length>=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.hash_shift^e.window[e.strstart+x-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart),0!=--e.prev_length;);if(e.match_available=0,e.match_length=x-1,e.strstart++,n&&(N(e,!1),0===e.strm.avail_out))return A}else if(e.match_available){if((n=u._tr_tally(e,0,e.window[e.strstart-1]))&&N(e,!1),e.strstart++,e.lookahead--,0===e.strm.avail_out)return A}else e.match_available=1,e.strstart++,e.lookahead--}return e.match_available&&(n=u._tr_tally(e,0,e.window[e.strstart-1]),e.match_available=0),e.insert=e.strstart<x-1?e.strstart:x-1,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}function M(e,t,r,n,i){this.good_length=e,this.max_lazy=t,this.nice_length=r,this.max_chain=n,this.func=i}function H(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=v,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new c.Buf16(2*w),this.dyn_dtree=new c.Buf16(2*(2*a+1)),this.bl_tree=new c.Buf16(2*(2*o+1)),D(this.dyn_ltree),D(this.dyn_dtree),D(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new c.Buf16(k+1),this.heap=new c.Buf16(2*s+1),D(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new c.Buf16(2*s+1),D(this.depth),this.l_buf=0,this.lit_bufsize=0,this.last_lit=0,this.d_buf=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}function G(e){var t;return e&&e.state?(e.total_in=e.total_out=0,e.data_type=i,(t=e.state).pending=0,t.pending_out=0,t.wrap<0&&(t.wrap=-t.wrap),t.status=t.wrap?C:E,e.adler=2===t.wrap?0:1,t.last_flush=l,u._tr_init(t),m):R(e,_)}function K(e){var t=G(e);return t===m&&function(e){e.window_size=2*e.w_size,D(e.head),e.max_lazy_match=h[e.level].max_lazy,e.good_match=h[e.level].good_length,e.nice_match=h[e.level].nice_length,e.max_chain_length=h[e.level].max_chain,e.strstart=0,e.block_start=0,e.lookahead=0,e.insert=0,e.match_length=e.prev_length=x-1,e.match_available=0,e.ins_h=0}(e.state),t}function Y(e,t,r,n,i,s){if(!e)return _;var a=1;if(t===g&&(t=6),n<0?(a=0,n=-n):15<n&&(a=2,n-=16),i<1||y<i||r!==v||n<8||15<n||t<0||9<t||s<0||b<s)return R(e,_);8===n&&(n=9);var o=new H;return(e.state=o).strm=e,o.wrap=a,o.gzhead=null,o.w_bits=n,o.w_size=1<<o.w_bits,o.w_mask=o.w_size-1,o.hash_bits=i+7,o.hash_size=1<<o.hash_bits,o.hash_mask=o.hash_size-1,o.hash_shift=~~((o.hash_bits+x-1)/x),o.window=new c.Buf8(2*o.w_size),o.head=new c.Buf16(o.hash_size),o.prev=new c.Buf16(o.w_size),o.lit_bufsize=1<<i+6,o.pending_buf_size=4*o.lit_bufsize,o.pending_buf=new c.Buf8(o.pending_buf_size),o.d_buf=1*o.lit_bufsize,o.l_buf=3*o.lit_bufsize,o.level=t,o.strategy=s,o.method=r,K(e)}h=[new M(0,0,0,0,function(e,t){var r=65535;for(r>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<t||t<0)return e?R(e,_):_;if(n=e.state,!e.output||!e.input&&0!==e.avail_in||666===n.status&&t!==f)return R(e,0===e.avail_out?-5:_);if(n.strm=e,r=n.last_flush,n.last_flush=t,n.status===C)if(2===n.wrap)e.adler=0,U(n,31),U(n,139),U(n,8),n.gzhead?(U(n,(n.gzhead.text?1:0)+(n.gzhead.hcrc?2:0)+(n.gzhead.extra?4:0)+(n.gzhead.name?8:0)+(n.gzhead.comment?16:0)),U(n,255&n.gzhead.time),U(n,n.gzhead.time>>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.gzindex<n.gzhead.name.length?255&n.gzhead.name.charCodeAt(n.gzindex++):0,U(n,s)}while(0!==s);n.gzhead.hcrc&&n.pending>i&&(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.gzindex<n.gzhead.comment.length?255&n.gzhead.comment.charCodeAt(n.gzindex++):0,U(n,s)}while(0!==s);n.gzhead.hcrc&&n.pending>i&&(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&&0<e.strstart&&(n=a[i=e.strstart-1])===a[++i]&&n===a[++i]&&n===a[++i]){s=e.strstart+S;do{}while(n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&i<s);e.match_length=S-(s-i),e.match_length>e.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<n.wrap&&(n.wrap=-n.wrap),0!==n.pending?m:1)},r.deflateEnd=function(e){var t;return e&&e.state?(t=e.state.status)!==C&&69!==t&&73!==t&&91!==t&&103!==t&&t!==E&&666!==t?R(e,_):(e.state=null,t===E?R(e,-3):m):_},r.deflateSetDictionary=function(e,t){var r,n,i,s,a,o,h,u,l=t.length;if(!e||!e.state)return _;if(2===(s=(r=e.state).wrap)||1===s&&r.status!==C||r.lookahead)return _;for(1===s&&(e.adler=d(e.adler,t,l,0)),r.wrap=0,l>=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<<r.hash_shift^r.window[n+x-1])&r.hash_mask,r.prev[n&r.w_mask]=r.head[r.ins_h],r.head[r.ins_h]=n,n++,--i;);r.strstart=n,r.lookahead=x-1,j(r)}return r.strstart+=r.lookahead,r.block_start=r.strstart,r.insert=r.lookahead,r.lookahead=0,r.match_length=r.prev_length=x-1,r.match_available=0,e.next_in=o,e.input=h,e.avail_in=a,r.wrap=s,m},r.deflateInfo="pako deflate (from Nodeca project)"},{"../utils/common":41,"./adler32":43,"./crc32":45,"./messages":51,"./trees":52}],47:[function(e,t,r){"use strict";t.exports=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1}},{}],48:[function(e,t,r){"use strict";t.exports=function(e,t){var r,n,i,s,a,o,h,u,l,f,c,d,p,m,_,g,b,v,y,w,k,x,S,z,C;r=e.state,n=e.next_in,z=e.input,i=n+(e.avail_in-5),s=e.next_out,C=e.output,a=s-(t-e.avail_out),o=s+(e.avail_out-257),h=r.dmax,u=r.wsize,l=r.whave,f=r.wnext,c=r.window,d=r.hold,p=r.bits,m=r.lencode,_=r.distcode,g=(1<<r.lenbits)-1,b=(1<<r.distbits)-1;e:do{p<15&&(d+=z[n++]<<p,p+=8,d+=z[n++]<<p,p+=8),v=m[d&g];t:for(;;){if(d>>>=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)-1)];continue t}if(32&y){r.mode=12;break e}e.msg="invalid literal/length code",r.mode=30;break e}w=65535&v,(y&=15)&&(p<y&&(d+=z[n++]<<p,p+=8),w+=d&(1<<y)-1,d>>>=y,p-=y),p<15&&(d+=z[n++]<<p,p+=8,d+=z[n++]<<p,p+=8),v=_[d&b];r:for(;;){if(d>>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(d&(1<<y)-1)];continue r}e.msg="invalid distance code",r.mode=30;break e}if(k=65535&v,p<(y&=15)&&(d+=z[n++]<<p,(p+=8)<y&&(d+=z[n++]<<p,p+=8)),h<(k+=d&(1<<y)-1)){e.msg="invalid distance too far back",r.mode=30;break e}if(d>>>=y,p-=y,(y=s-a)<k){if(l<(y=k-y)&&r.sane){e.msg="invalid distance too far back",r.mode=30;break e}if(S=c,(x=0)===f){if(x+=u-y,y<w){for(w-=y;C[s++]=c[x++],--y;);x=s-k,S=C}}else if(f<y){if(x+=u+f-y,(y-=f)<w){for(w-=y;C[s++]=c[x++],--y;);if(x=0,f<w){for(w-=y=f;C[s++]=c[x++],--y;);x=s-k,S=C}}}else if(x+=f-y,y<w){for(w-=y;C[s++]=c[x++],--y;);x=s-k,S=C}for(;2<w;)C[s++]=S[x++],C[s++]=S[x++],C[s++]=S[x++],w-=3;w&&(C[s++]=S[x++],1<w&&(C[s++]=S[x++]))}else{for(x=s-k;C[s++]=C[x++],C[s++]=C[x++],C[s++]=C[x++],2<(w-=3););w&&(C[s++]=C[x++],1<w&&(C[s++]=C[x++]))}break}}break}}while(n<i&&s<o);n-=w=p>>3,d&=(1<<(p-=w<<3))-1,e.next_in=n,e.next_out=s,e.avail_in=n<i?i-n+5:5-(n-i),e.avail_out=s<o?o-s+257:257-(s-o),r.hold=d,r.bits=p}},{}],49:[function(e,t,r){"use strict";var I=e("../utils/common"),O=e("./adler32"),B=e("./crc32"),R=e("./inffast"),T=e("./inftrees"),D=1,F=2,N=0,U=-2,P=1,n=852,i=592;function L(e){return(e>>>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<t)?U:(null!==n.window&&n.wbits!==t&&(n.window=null),n.wrap=r,n.wbits=t,o(e))):U}function u(e,t){var r,n;return e?(n=new s,(e.state=n).window=null,(r=h(e,t))!==N&&(e.state=null),r):U}var l,f,c=!0;function j(e){if(c){var t;for(l=new I.Buf32(512),f=new I.Buf32(32),t=0;t<144;)e.lens[t++]=8;for(;t<256;)e.lens[t++]=9;for(;t<280;)e.lens[t++]=7;for(;t<288;)e.lens[t++]=8;for(T(D,e.lens,0,288,l,0,e.work,{bits:9}),t=0;t<32;)e.lens[t++]=5;T(F,e.lens,0,32,f,0,e.work,{bits:5}),c=!1}e.lencode=l,e.lenbits=9,e.distcode=f,e.distbits=5}function Z(e,t,r,n){var i,s=e.state;return null===s.window&&(s.wsize=1<<s.wbits,s.wnext=0,s.whave=0,s.window=new I.Buf8(s.wsize)),n>=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<s.wsize&&(s.whave+=i))),0}r.inflateReset=o,r.inflateReset2=h,r.inflateResetKeep=a,r.inflateInit=function(e){return u(e,15)},r.inflateInit2=u,r.inflate=function(e,t){var r,n,i,s,a,o,h,u,l,f,c,d,p,m,_,g,b,v,y,w,k,x,S,z,C=0,E=new I.Buf8(4),A=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15];if(!e||!e.state||!e.output||!e.input&&0!==e.avail_in)return U;12===(r=e.state).mode&&(r.mode=13),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,f=o,c=h,x=N;e:for(;;)switch(r.mode){case P:if(0===r.wrap){r.mode=13;break}for(;l<16;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(2&r.wrap&&35615===u){E[r.check=0]=255&u,E[1]=u>>>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<<k,e.adler=r.check=1,r.mode=512&u?10:12,l=u=0;break;case 2:for(;l<16;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(r.flags=u,8!=(255&r.flags)){e.msg="unknown compression method",r.mode=30;break}if(57344&r.flags){e.msg="unknown header flags set",r.mode=30;break}r.head&&(r.head.text=u>>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++]<<l,l+=8}r.head&&(r.head.time=u),512&r.flags&&(E[0]=255&u,E[1]=u>>>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++]<<l,l+=8}r.head&&(r.head.xflags=255&u,r.head.os=u>>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++]<<l,l+=8}r.length=u,r.head&&(r.head.extra_len=u),512&r.flags&&(E[0]=255&u,E[1]=u>>>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<o;);if(512&r.flags&&(r.check=B(r.check,n,d,s)),o-=d,s+=d,k)break e}else r.head&&(r.head.name=null);r.length=0,r.mode=8;case 8:if(4096&r.flags){if(0===o)break e;for(d=0;k=n[s+d++],r.head&&k&&r.length<65536&&(r.head.comment+=String.fromCharCode(k)),k&&d<o;);if(512&r.flags&&(r.check=B(r.check,n,d,s)),o-=d,s+=d,k)break e}else r.head&&(r.head.comment=null);r.mode=9;case 9:if(512&r.flags){for(;l<16;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(u!==(65535&r.check)){e.msg="header crc mismatch",r.mode=30;break}l=u=0}r.head&&(r.head.hcrc=r.flags>>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++]<<l,l+=8}e.adler=r.check=L(u),l=u=0,r.mode=11;case 11:if(0===r.havedict)return e.next_out=a,e.avail_out=h,e.next_in=s,e.avail_in=o,r.hold=u,r.bits=l,2;e.adler=r.check=1,r.mode=12;case 12:if(5===t||6===t)break e;case 13:if(r.last){u>>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}switch(r.last=1&u,l-=1,3&(u>>>=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++]<<l,l+=8}if((65535&u)!=(u>>>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<d&&(d=o),h<d&&(d=h),0===d)break e;I.arraySet(i,n,s,d,a),o-=d,s+=d,h-=d,a+=d,r.length-=d;break}r.mode=12;break;case 17:for(;l<14;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(r.nlen=257+(31&u),u>>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286<r.nlen||30<r.ndist){e.msg="too many length or distance symbols",r.mode=30;break}r.have=0,r.mode=18;case 18:for(;r.have<r.ncode;){for(;l<3;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}r.lens[A[r.have++]]=7&u,u>>>=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<r.nlen+r.ndist;){for(;g=(C=r.lencode[u&(1<<r.lenbits)-1])>>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(b<16)u>>>=_,l-=_,r.lens[r.have++]=b;else{if(16===b){for(z=_+2;l<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(u>>>=_,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<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}l-=_,k=0,d=3+(7&(u>>>=_)),u>>>=3,l-=3}else{for(z=_+7;l<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}l-=_,k=0,d=11+(127&(u>>>=_)),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<<r.lenbits)-1])>>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(g&&0==(240&g)){for(v=_,y=g,w=b;g=(C=r.lencode[w+((u&(1<<v+y)-1)>>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}u>>>=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<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}r.length+=u&(1<<r.extra)-1,u>>>=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<<r.distbits)-1])>>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(0==(240&g)){for(v=_,y=g,w=b;g=(C=r.distcode[w+((u&(1<<v+y)-1)>>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}u>>>=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<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}r.offset+=u&(1<<r.extra)-1,u>>>=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(h<d&&(d=h),h-=d,r.length-=d;i[a++]=m[p++],--d;);0===r.length&&(r.mode=21);break;case 26:if(0===h)break e;i[a++]=r.length,h--,r.mode=21;break;case 27:if(r.wrap){for(;l<32;){if(0===o)break e;o--,u|=n[s++]<<l,l+=8}if(c-=h,e.total_out+=c,r.total+=c,c&&(e.adler=r.check=r.flags?B(r.check,i,c,a-c):O(r.check,i,c,a-c)),c=h,(r.flags?u:L(u))!==r.check){e.msg="incorrect data check",r.mode=30;break}l=u=0}r.mode=28;case 28:if(r.wrap&&r.flags){for(;l<32;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(u!==(4294967295&r.total)){e.msg="incorrect length check",r.mode=30;break}l=u=0}r.mode=29;case 29:x=1;break e;case 30:x=-3;break e;case 31:return-4;case 32:default:return U}return e.next_out=a,e.avail_out=h,e.next_in=s,e.avail_in=o,r.hold=u,r.bits=l,(r.wsize||c!==e.avail_out&&r.mode<30&&(r.mode<27||4!==t))&&Z(e,e.output,e.next_out,c-e.avail_out)?(r.mode=31,-4):(f-=e.avail_in,c-=e.avail_out,e.total_in+=f,e.total_out+=c,r.total+=c,r.wrap&&c&&(e.adler=r.check=r.flags?B(r.check,i,c,e.next_out-c):O(r.check,i,c,e.next_out-c)),e.data_type=r.bits+(r.last?64:0)+(12===r.mode?128:0)+(20===r.mode||15===r.mode?256:0),(0==f&&0===c||4===t)&&x===N&&(x=-5),x)},r.inflateEnd=function(e){if(!e||!e.state)return U;var t=e.state;return t.window&&(t.window=null),e.state=null,N},r.inflateGetHeader=function(e,t){var r;return e&&e.state?0==(2&(r=e.state).wrap)?U:((r.head=t).done=!1,N):U},r.inflateSetDictionary=function(e,t){var r,n=t.length;return e&&e.state?0!==(r=e.state).wrap&&11!==r.mode?U:11===r.mode&&O(1,t,n,0)!==r.check?-3:Z(e,t,n,n)?(r.mode=31,-4):(r.havedict=1,N):U},r.inflateInfo="pako inflate (from Nodeca project)"},{"../utils/common":41,"./adler32":43,"./crc32":45,"./inffast":48,"./inftrees":50}],50:[function(e,t,r){"use strict";var D=e("../utils/common"),F=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],N=[16,16,16,16,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,20,20,20,20,21,21,21,21,16,72,78],U=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0],P=[16,16,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,64,64];t.exports=function(e,t,r,n,i,s,a,o){var h,u,l,f,c,d,p,m,_,g=o.bits,b=0,v=0,y=0,w=0,k=0,x=0,S=0,z=0,C=0,E=0,A=null,I=0,O=new D.Buf16(16),B=new D.Buf16(16),R=null,T=0;for(b=0;b<=15;b++)O[b]=0;for(v=0;v<n;v++)O[t[r+v]]++;for(k=g,w=15;1<=w&&0===O[w];w--);if(w<k&&(k=w),0===w)return i[s++]=20971520,i[s++]=20971520,o.bits=1,0;for(y=1;y<w&&0===O[y];y++);for(k<y&&(k=y),b=z=1;b<=15;b++)if(z<<=1,(z-=O[b])<0)return-1;if(0<z&&(0===e||1!==w))return-1;for(B[1]=0,b=1;b<15;b++)B[b+1]=B[b]+O[b];for(v=0;v<n;v++)0!==t[r+v]&&(a[B[t[r+v]]++]=v);if(d=0===e?(A=R=a,19):1===e?(A=F,I-=257,R=N,T-=257,256):(A=U,R=P,-1),b=y,c=s,S=v=E=0,l=-1,f=(C=1<<(x=k))-1,1===e&&852<C||2===e&&592<C)return 1;for(;;){for(p=b-S,_=a[v]<d?(m=0,a[v]):a[v]>d?(m=R[T+a[v]],A[I+a[v]]):(m=96,0),h=1<<b-S,y=u=1<<x;i[c+(E>>S)+(u-=h)]=p<<24|m<<16|_|0,0!==u;);for(h=1<<b-1;E&h;)h>>=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<b&&(E&f)!==l){for(0===S&&(S=k),c+=y,z=1<<(x=b-S);x+S<w&&!((z-=O[x+S])<=0);)x++,z<<=1;if(C+=1<<x,1===e&&852<C||2===e&&592<C)return 1;i[l=E&f]=k<<24|x<<16|c-s|0}}return 0!==E&&(i[c+E]=b-S<<24|64<<16|0),o.bits=k,0}},{"../utils/common":41}],51:[function(e,t,r){"use strict";t.exports={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"}},{}],52:[function(e,t,r){"use strict";var i=e("../utils/common"),o=0,h=1;function n(e){for(var t=e.length;0<=--t;)e[t]=0}var s=0,a=29,u=256,l=u+1+a,f=30,c=19,_=2*l+1,g=15,d=16,p=7,m=256,b=16,v=17,y=18,w=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],k=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],x=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],S=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],z=new Array(2*(l+2));n(z);var C=new Array(2*f);n(C);var E=new Array(512);n(E);var A=new Array(256);n(A);var I=new Array(a);n(I);var O,B,R,T=new Array(f);function D(e,t,r,n,i){this.static_tree=e,this.extra_bits=t,this.extra_base=r,this.elems=n,this.max_length=i,this.has_stree=e&&e.length}function F(e,t){this.dyn_tree=e,this.max_code=0,this.stat_desc=t}function N(e){return e<256?E[e]:E[256+(e>>>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<<e.bi_valid&65535,U(e,e.bi_buf),e.bi_buf=t>>d-e.bi_valid,e.bi_valid+=r-d):(e.bi_buf|=t<<e.bi_valid&65535,e.bi_valid+=r)}function L(e,t,r){P(e,r[2*t],r[2*t+1])}function j(e,t){for(var r=0;r|=1&e,e>>>=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<l;t++)e.dyn_ltree[2*t]=0;for(t=0;t<f;t++)e.dyn_dtree[2*t]=0;for(t=0;t<c;t++)e.bl_tree[2*t]=0;e.dyn_ltree[2*m]=1,e.opt_len=e.static_len=0,e.last_lit=e.matches=0}function M(e){8<e.bi_valid?U(e,e.bi_buf):0<e.bi_valid&&(e.pending_buf[e.pending++]=e.bi_buf),e.bi_buf=0,e.bi_valid=0}function H(e,t,r,n){var i=2*t,s=2*r;return e[i]<e[s]||e[i]===e[s]&&n[t]<=n[r]}function G(e,t,r){for(var n=e.heap[r],i=r<<1;i<=e.heap_len&&(i<e.heap_len&&H(t,e.heap[i+1],e.heap[i],e.depth)&&i++,!H(t,n,e.heap[i],e.depth));)e.heap[r]=e.heap[i],r=i,i<<=1;e.heap[r]=n}function K(e,t,r){var n,i,s,a,o=0;if(0!==e.last_lit)for(;n=e.pending_buf[e.d_buf+2*o]<<8|e.pending_buf[e.d_buf+2*o+1],i=e.pending_buf[e.l_buf+o],o++,0===n?L(e,i,t):(L(e,(s=A[i])+u+1,t),0!==(a=w[s])&&P(e,i-=I[s],a),L(e,s=N(--n),r),0!==(a=k[s])&&P(e,n-=T[s],a)),o<e.last_lit;);L(e,m,t)}function Y(e,t){var r,n,i,s=t.dyn_tree,a=t.stat_desc.static_tree,o=t.stat_desc.has_stree,h=t.stat_desc.elems,u=-1;for(e.heap_len=0,e.heap_max=_,r=0;r<h;r++)0!==s[2*r]?(e.heap[++e.heap_len]=u=r,e.depth[r]=0):s[2*r+1]=0;for(;e.heap_len<2;)s[2*(i=e.heap[++e.heap_len]=u<2?++u:0)]=1,e.depth[i]=0,e.opt_len--,o&&(e.static_len-=a[2*i+1]);for(t.max_code=u,r=e.heap_len>>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<n||(e.bl_count[s]++,a=0,d<=n&&(a=c[n-d]),o=h[2*n],e.opt_len+=o*(s+a),f&&(e.static_len+=o*(l[2*n+1]+a)));if(0!==m){do{for(s=p-1;0===e.bl_count[s];)s--;e.bl_count[s]--,e.bl_count[s+1]+=2,e.bl_count[p]--,m-=2}while(0<m);for(s=p;0!==s;s--)for(n=e.bl_count[s];0!==n;)u<(i=e.heap[--r])||(h[2*i+1]!==s&&(e.opt_len+=(s-h[2*i+1])*h[2*i],h[2*i+1]=s),n--)}}(e,t),Z(s,u,e.bl_count)}function X(e,t,r){var n,i,s=-1,a=t[1],o=0,h=7,u=4;for(0===a&&(h=138,u=3),t[2*(r+1)+1]=65535,n=0;n<=r;n++)i=a,a=t[2*(n+1)+1],++o<h&&i===a||(o<u?e.bl_tree[2*i]+=o:0!==i?(i!==s&&e.bl_tree[2*i]++,e.bl_tree[2*b]++):o<=10?e.bl_tree[2*v]++:e.bl_tree[2*y]++,s=i,u=(o=0)===a?(h=138,3):i===a?(h=6,3):(h=7,4))}function V(e,t,r){var n,i,s=-1,a=t[1],o=0,h=7,u=4;for(0===a&&(h=138,u=3),n=0;n<=r;n++)if(i=a,a=t[2*(n+1)+1],!(++o<h&&i===a)){if(o<u)for(;L(e,i,e.bl_tree),0!=--o;);else 0!==i?(i!==s&&(L(e,i,e.bl_tree),o--),L(e,b,e.bl_tree),P(e,o-3,2)):o<=10?(L(e,v,e.bl_tree),P(e,o-3,3)):(L(e,y,e.bl_tree),P(e,o-11,7));s=i,u=(o=0)===a?(h=138,3):i===a?(h=6,3):(h=7,4)}}n(T);var q=!1;function J(e,t,r,n){P(e,(s<<1)+(n?1:0),3),function(e,t,r,n){M(e),n&&(U(e,r),U(e,~r)),i.arraySet(e.pending_buf,e.window,t,r,e.pending),e.pending+=r}(e,t,r,!0)}r._tr_init=function(e){q||(function(){var e,t,r,n,i,s=new Array(g+1);for(n=r=0;n<a-1;n++)for(I[n]=r,e=0;e<1<<w[n];e++)A[r++]=n;for(A[r-1]=n,n=i=0;n<16;n++)for(T[n]=i,e=0;e<1<<k[n];e++)E[i++]=n;for(i>>=7;n<f;n++)for(T[n]=i<<7,e=0;e<1<<k[n]-7;e++)E[256+i++]=n;for(t=0;t<=g;t++)s[t]=0;for(e=0;e<=143;)z[2*e+1]=8,e++,s[8]++;for(;e<=255;)z[2*e+1]=9,e++,s[9]++;for(;e<=279;)z[2*e+1]=7,e++,s[7]++;for(;e<=287;)z[2*e+1]=8,e++,s[8]++;for(Z(z,l+1,s),e=0;e<f;e++)C[2*e+1]=5,C[2*e]=j(e,5);O=new D(z,w,u+1,l,g),B=new D(C,k,0,f,g),R=new D(new Array(0),x,0,c,p)}(),q=!0),e.l_desc=new F(e.dyn_ltree,O),e.d_desc=new F(e.dyn_dtree,B),e.bl_desc=new F(e.bl_tree,R),e.bi_buf=0,e.bi_valid=0,W(e)},r._tr_stored_block=J,r._tr_flush_block=function(e,t,r,n){var i,s,a=0;0<e.level?(2===e.strm.data_type&&(e.strm.data_type=function(e){var t,r=4093624447;for(t=0;t<=31;t++,r>>>=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<u;t++)if(0!==e.dyn_ltree[2*t])return h;return o}(e)),Y(e,e.l_desc),Y(e,e.d_desc),a=function(e){var t;for(X(e,e.dyn_ltree,e.l_desc.max_code),X(e,e.dyn_dtree,e.d_desc.max_code),Y(e,e.bl_desc),t=c-1;3<=t&&0===e.bl_tree[2*S[t]+1];t--);return e.opt_len+=3*(t+1)+5+5+4,t}(e),i=e.opt_len+3+7>>>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<n;i++)P(e,e.bl_tree[2*S[i]+1],3);V(e,e.dyn_ltree,t-1),V(e,e.dyn_dtree,r-1)}(e,e.l_desc.max_code+1,e.d_desc.max_code+1,a+1),K(e,e.dyn_ltree,e.dyn_dtree)),W(e),n&&M(e)},r._tr_tally=function(e,t,r){return e.pending_buf[e.d_buf+2*e.last_lit]=t>>>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<t.length;r++)t[r]=arguments[r+1];var n={callback:e,args:t};return h[o]=n,i(o),o++},e.clearImmediate=f}function f(e){delete h[e]}function c(e){if(u)setTimeout(c,0,e);else{var t=h[e];if(t){u=!0;try{!function(e){var t=e.callback,r=e.args;switch(r.length){case 0:t();break;case 1:t(r[0]);break;case 2:t(r[0],r[1]);break;case 3:t(r[0],r[1],r[2]);break;default:t.apply(n,r)}}(t)}finally{f(e),u=!1}}}}function d(e){e.source===r&&"string"==typeof e.data&&0===e.data.indexOf(a)&&c(+e.data.slice(a.length))}}("undefined"==typeof self?void 0===e?this:e:self)}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}]},{},[10])(10)});
window.JSZip = module.exports;
})();
/* QuestionBankCore: 从已保存或当前练习页面读取题目,始终以“参考答案”为准。 */
(function (root) {
'use strict';
const SELECTORS = {
question: '.pc_examtest_content',
stem: '.pc_examtest_content_question',
option: '.pc_examtest_content_list_des',
label: '.examtest_content_list_des_optionsort',
reference: '.examtest_content_answer_left',
explanation: '.pc_examtest_content_resolve',
card: '.examtestcard_count_defult'
};
// 考试页的分数和难度属于界面标记;只按 DOM class 去除,保留题干中的括号和数字。
const CONTROL = '.pc_examtest_content_question_showanswer,.pc_examtest_content_question_value,.pc_examtest_content_question_difficulty,script,style,button,input,select,textarea,iframe,object,embed,link';
const TYPE_RE = /^\s*[\[【](单选题?|多选题?|判断题?|填空题?|简答题?|问答题?)[\]】]\s*/;
const NUMBER_RE = /^\s*(\d+)\s*[..、]\s*/;
function normalize(value) {
return String(value == null ? '' : value).replace(/\u00a0/g, ' ').replace(/[\t\n\r ]+/g, ' ').trim();
}
function cleanClone(node) {
if (!node) return null;
const copy = node.cloneNode(true);
copy.querySelectorAll(CONTROL).forEach(function (item) { item.remove(); });
return copy;
}
function textOf(node) {
const copy = cleanClone(node);
return copy ? normalize(copy.textContent) : '';
}
function savedSourceURL(doc) {
const walker = doc.createTreeWalker(doc, 128);
let comment;
while ((comment = walker.nextNode())) {
const match = comment.nodeValue.match(/saved from url=\(\d+\)([\s\S]+?)\s*$/i);
if (match) return match[1].trim();
}
return '';
}
function usableURL(value) {
return /^(https?:|file:|blob:)/i.test(value || '') ? value : '';
}
function resolveURL(raw, base) {
if (!raw) return '';
raw = raw.trim();
if (/^(javascript|vbscript):/i.test(raw) || /^data:(?!image\/)/i.test(raw)) return '';
try { return new URL(raw, base || undefined).href; } catch (_) { return raw; }
}
function originalAttribute(node, name) {
return node.getAttribute('data-qb-original-' + name) || node.getAttribute(name) || '';
}
function baseURL(doc, sourceUrl) {
const base = doc.querySelector('base');
return base ? resolveURL(originalAttribute(base, 'href'), sourceUrl) || sourceUrl : sourceUrl;
}
function imageResources(node, base) {
if (!node) return [];
return Array.from(node.querySelectorAll('img')).map(function (img) {
const originalSrc = originalAttribute(img, 'src') || img.getAttribute('data-src') || img.getAttribute('data-original') || '';
return {
src: originalSrc,
url: resolveURL(originalSrc, base),
alt: normalize(img.getAttribute('alt'))
};
}).filter(function (item) { return item.url; });
}
function uniqueImages(resources) {
return Array.from(new Set(resources.map(function (item) { return item.url; })));
}
// HTML 仅作为附加数据导出。预览界面只使用 textContent,不插入这些 HTML。
function staticHTML(node, base) {
const copy = cleanClone(node);
if (!copy) return '';
const linkURLs = Array.from(copy.querySelectorAll('a')).map(function (link) { return resolveURL(originalAttribute(link, 'href'), base); });
copy.querySelectorAll('*').forEach(function (element) {
Array.from(element.attributes).forEach(function (attr) {
if (/^on|^_ng|^ng-|^style$|^data-qb-original-/i.test(attr.name)) element.removeAttribute(attr.name);
});
});
const sourceImages = node.querySelectorAll('img');
copy.querySelectorAll('img').forEach(function (img, index) {
const source = sourceImages[index];
const raw = source && (originalAttribute(source, 'src') || source.getAttribute('data-src') || source.getAttribute('data-original'));
const url = resolveURL(raw, base);
if (url) img.setAttribute('src', url); else img.removeAttribute('src');
img.removeAttribute('srcset');
});
copy.querySelectorAll('a').forEach(function (link, index) {
const href = linkURLs[index];
if (href) link.setAttribute('href', href); else link.removeAttribute('href');
});
return copy.innerHTML.trim();
}
function inferType(stem, options) {
const match = stem.match(TYPE_RE);
if (match) return match[1].replace(/题$/, '');
if (options.some(function (option) { return option.node.querySelector('.examtest_content_list_des_multiplesort'); })) return '多选';
const texts = options.map(function (option) { return option.text; });
if (texts.length === 2 && texts.every(function (value) { return /^(正确|错误|对|错|是|否)$/.test(value); })) return '判断';
return '未知';
}
function readAnswer(box) {
const reference = box.querySelector(SELECTORS.reference);
if (!reference) return '';
const pieces = Array.from(reference.querySelectorAll('.answer')).map(textOf).filter(Boolean);
if (pieces.length) {
// 多选可保存为一个 ABC span,也可分别保存为 A、B、C 多个 span。
if (pieces.every(function (piece) { return /^[A-Z\s,,、;;]+$/i.test(piece); })) return pieces.join('').replace(/[\s,,、;;]+/g, '').toUpperCase();
return pieces.join(';');
}
return textOf(reference).replace(/^\s*参考答案\s*[::]\s*/, '').trim();
}
function answerOptions(answer, options) {
if (!answer) return [];
const normalizedAnswer = answer.replace(/[\s,,、;;]+/g, '').toUpperCase();
const letterLabels = options.every(function (option) { return /^[A-Z]$/i.test(option.label); });
if (letterLabels && /^[A-Z]+$/.test(normalizedAnswer)) {
return options.filter(function (option) { return normalizedAnswer.includes(option.label.toUpperCase()); });
}
const pieces = answer.split(/[\s,,、;;]+/).filter(Boolean);
return options.filter(function (option) { return pieces.includes(option.label) || pieces.includes(option.text); });
}
function extract(doc, sourceUrl) {
if (!doc || typeof doc.querySelectorAll !== 'function') throw new TypeError('extract 需要 Document 对象。');
const source = usableURL(sourceUrl) || usableURL(doc.__questionBankSourceURL) || savedSourceURL(doc) || usableURL(doc.URL) || '';
const resourceBase = baseURL(doc, source);
const title = textOf(doc.querySelector('.pc_examtest_title')) || normalize(doc.title) || '练习题库';
const questions = Array.from(doc.querySelectorAll(SELECTORS.question)).map(function (box, index) {
const stemNode = box.querySelector(SELECTORS.stem);
const rawStem = textOf(stemNode);
const optionItems = Array.from(box.querySelectorAll(SELECTORS.option)).map(function (node, optionIndex) {
const label = textOf(node.querySelector(SELECTORS.label)) || String.fromCharCode(65 + optionIndex);
const copy = cleanClone(node);
copy.querySelectorAll(SELECTORS.label).forEach(function (item) { item.remove(); });
// 保存网页把选项图片放在与文字相邻的 _list_img 内。
const container = node.parentElement;
const resources = imageResources(container || node, resourceBase);
return { node: node, label: label, text: normalize(copy.textContent), images: uniqueImages(resources), imageResources: resources, html: staticHTML(copy, resourceBase) };
});
const type = inferType(rawStem, optionItems);
const withoutType = rawStem.replace(TYPE_RE, '');
const numberMatch = withoutType.match(NUMBER_RE);
const number = numberMatch ? Number(numberMatch[1]) : index + 1;
const question = withoutType.replace(NUMBER_RE, '').trim();
const options = optionItems.map(function (option) {
return { label: option.label, text: option.text, images: option.images, imageResources: option.imageResources, html: option.html };
});
const answer = readAnswer(box);
const matchedOptions = answerOptions(answer, options);
const resolveNode = box.querySelector(SELECTORS.explanation);
const explanation = textOf(resolveNode).replace(/^\s*(?:题目解析|答案解析|解析)\s*[::]\s*/, '').trim();
const questionResources = imageResources(stemNode, resourceBase);
const explanationResources = imageResources(resolveNode, resourceBase);
const allResources = questionResources.concat(options.flatMap(function (option) { return option.imageResources; }), explanationResources);
return {
number: number,
type: type,
question: question,
options: options,
answer: answer,
answerText: matchedOptions.map(function (option) { return option.label + '. ' + (option.text || '[图片选项]'); }).join(';'),
explanation: explanation,
images: uniqueImages(allResources),
questionImages: uniqueImages(questionResources),
explanationImages: uniqueImages(explanationResources),
imageResources: allResources,
questionHTML: staticHTML(stemNode, resourceBase),
explanationHTML: staticHTML(resolveNode, resourceBase)
};
});
const cardCount = doc.querySelectorAll(SELECTORS.card).length;
const expectedTotal = cardCount || null;
const missingAnswers = questions.filter(function (question) { return !question.answer; }).map(function (question) { return question.number; });
return {
title: title,
sourceUrl: source,
resourceBaseUrl: resourceBase,
exportedAt: new Date().toISOString(),
total: questions.length,
answered: questions.length - missingAnswers.length,
missingAnswers: missingAnswers,
expectedTotal: expectedTotal,
complete: questions.length > 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(/<script\b[^>]*>[\s\S]*?<\/script\s*>/gi, '').replace(/<style\b[^>]*>[\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.v3';
const LEGACY_BANK_KEY = 'yianxue.assistant.bank.v2';
const SETTING_PREFIX = 'yianxue.assistant.setting.v2.';
const MAX_BYTES = 60 * 1024 * 1024;
let current = null;
let loading = 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, 未标明: 0 };
const questions = new Map(), variants = new Set();
records.forEach(record => {
const role = record.role || '未标明';
counts[role] = (counts[role] || 0) + 1;
const key = record.questionKey + '\n' + record.optionKey;
const answer = JSON.stringify(record.correctTexts.map(normalize).sort());
if (!questions.has(key)) questions.set(key, new Set());
questions.get(key).add(answer);
variants.add(key + '\n' + answer);
});
return { schemaVersion: 3, records, counts, total: records.length, uniqueTotal: variants.size, questionTotal: questions.size,
conflictTotal: Array.from(questions.values()).filter(answers => answers.size > 1).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();
// Role is provenance only. Older exports and role-free banks share one index.
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,
sourceFile: read(row, '来源文件') || context.sourceFile,
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.sourceFile, record.number, record.questionKey, record.optionKey, record.correctTexts.map(normalize).sort()]);
if (seen.has(key)) return false;
seen.add(key); return true;
});
}
async function readSaved() {
const backend = storage();
let saved = await backend.get(BANK_KEY, null);
const legacy = saved == null;
if (legacy) saved = await backend.get(LEGACY_BANK_KEY, null);
if (typeof saved === 'string') {
try { saved = JSON.parse(saved); } catch (_) { throw new Error('已缓存题库格式损坏;请重新导入题库'); }
}
if (saved == null) return snapshot([], 0, backend.name);
if (!saved || !Array.isArray(saved.records) || (!legacy && saved.schemaVersion !== 3)) 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); }
});
const next = snapshot(deduplicate(records), Number(saved.revision) || 0, backend.name);
if (saved.updatedAt) next.updatedAt = saved.updatedAt;
if (legacy) {
// Do not alter the v2 value: a failed migration always leaves a recoverable copy.
try {
await backend.set(BANK_KEY, next);
next.migration = { from: 2, status: 'complete' };
} catch (error) {
// The validated legacy bank remains usable when a storage quota is exhausted.
next.migration = { from: 2, status: 'pending', error: String(error.message || error) };
}
}
return next;
}
async function load() {
if (current) return current;
if (!loading) loading = readSaved().then(value => (current = value)).finally(() => { loading = null; });
return loading;
}
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 题库文件');
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: '3.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 unified local search. Roles remain source metadata. */
(function (root) {
'use strict';
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 => {
const key = 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 (!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(groupKey(question)) || [];
if (!records.length) return result('missing', '统一题库没有题型、题干和全部选项均一致的记录');
if (Array.isArray(context.courses)) {
const allowed = new Set(context.courses.map(courseNormalize).filter(Boolean));
records = records.filter(record => allowed.has(courseNormalize(record.course)));
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 empty = reason => ({ items: [], total: 0, reason, role: '', query: String(query || ''), hasMore: false });
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 (course && courseNormalize(record.course) !== course) return;
if (settings.sourceUrl && sourceIdentity(record.sourceUrl) !== sourceIdentity(settings.sourceUrl)) 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 || '', sourceFile: record.sourceFile || '', 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: '3.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 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, position: null,
activeTab: 'overview', uniqueTotal: 0, conflictTotal: 0,
learning: { status: 'idle', mode: 'all', execution: 'sequential', rate: 1, answerInterval: 4, minSubmitMinutes: 6, tasks: [], total: 0, completed: 0, skipped: 0, waiting: 0, failed: 0, etaSeconds: null, video: {}, exam: {}, logs: [], message: '' }
};
let destroyed = false;
let selectionText = '';
let currentOffset = 0;
let resultTotal = 0;
let drag = null;
let suppressHeaderClick = false;
let lastPositionInput = 'unset';
let appliedPosition = 'default';
let clampFrame = null;
let notifyClamp = false;
let resizeObserver = null;
let previousExpanded = false;
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}';
style.textContent += '.header{cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none}.panel.dragging .header{cursor:grabbing}.panel.dragging,.panel.dragging *{-webkit-user-select:none!important;user-select:none!important}';
style.textContent += `
.panel{width:640px;max-width:calc(100vw - 24px)}.panel.collapsed{width:auto;max-width:calc(100vw - 24px);min-width:210px}.brand{font-size:14px}.body{padding:0;overflow:hidden;max-height:calc(100vh - 88px);max-height:calc(100dvh - 88px);display:flex;flex-direction:column}.global-bar{flex:none;padding:11px 13px;border-bottom:1px solid var(--line);background:#f8faff}.global-top{display:flex;align-items:center;gap:8px}.global-status{font-weight:650;flex:1;min-width:0;overflow-wrap:anywhere}.global-action{font-size:12px;white-space:nowrap}.global-progress-row{display:flex;align-items:center;gap:9px;margin-top:7px}.global-progress{min-width:30px;flex:1;height:7px;border:0;border-radius:5px;overflow:hidden;accent-color:var(--accent)}.global-progress::-webkit-progress-bar{background:#e1e8f3;border-radius:5px}.global-progress::-webkit-progress-value{background:var(--accent);border-radius:5px}.global-progress-label{font-size:11px;color:var(--muted);white-space:nowrap}.worker-status{font-size:11px;color:var(--muted);margin-top:5px;white-space:pre-wrap;overflow-wrap:anywhere}.workspace{display:flex;min-height:0;height:500px;max-height:calc(100vh - 230px);max-height:calc(100dvh - 230px)}.navigation{display:flex;flex-direction:column;gap:4px;flex:none;width:123px;padding:12px 8px;border-right:1px solid var(--line);background:#f8faff;overflow:auto}.nav-tab{border:0;background:transparent;text-align:left;padding:9px 10px;border-radius:7px;font-size:12px;white-space:nowrap}.nav-tab[aria-selected=true]{background:#e6efff;color:#174fae;font-weight:700}.nav-tab[data-alert=true]::after{content:' ·';color:#ad6921;font-weight:800}.panes{flex:1;min-width:0;min-height:0}.tab-pane{height:100%;padding:13px;overflow:auto;overscroll-behavior:contain}.tab-pane>.section:first-child{padding-top:0}.tab-pane .page-section:first-child{margin-top:0;padding-top:0;border-top:0}.notice{flex:none;margin:0;padding:8px 13px;border-radius:0;max-height:65px;overflow:auto}.setting-row{display:grid;grid-template-columns:88px minmax(0,1fr);align-items:center;gap:9px;margin:10px 0}.setting-row>span{font-size:12px;color:#43546d}.setting-row input[type=number]{min-width:0;width:100%;border:1px solid #cfd8e7;border-radius:7px;padding:6px 8px;color:#17253d;background:white}.task-actions,.console-actions{display:flex;flex-wrap:wrap;gap:7px;margin:12px 0}.summary-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin:10px 0}.summary-card{padding:9px 10px;background:var(--surface);border:1px solid var(--line);border-radius:8px;overflow-wrap:anywhere}.summary-card strong{display:block;font-size:20px;font-weight:650;color:#214e91}.summary-card span{font-size:11px;color:var(--muted)}.worker-card{display:block;text-align:left;width:100%;margin-top:8px;padding:10px;white-space:pre-wrap;overflow-wrap:anywhere}.task-list{display:grid;gap:7px}.task-row{padding:9px 10px;border:1px solid var(--line);border-radius:7px;overflow-wrap:anywhere}.task-name{font-weight:600;margin:0 0 3px;font-size:12px}.task-state{font-size:11px;color:var(--muted)}.info-grid{display:grid;grid-template-columns:80px minmax(0,1fr);gap:7px;font-size:12px;margin:10px 0}.info-grid dt{color:var(--muted)}.info-grid dd{margin:0;overflow-wrap:anywhere;white-space:pre-wrap}.explanation{padding:9px 10px;background:var(--surface);border-radius:7px;font-size:12px;color:var(--muted);white-space:pre-wrap;overflow-wrap:anywhere}.mode-note{font-size:11px;color:#926214;margin:7px 0}.console-filter{max-width:125px}.log-list{display:grid;gap:6px;font-size:11px;-webkit-user-select:text;user-select:text}.log-entry{padding:7px 8px;border-radius:5px;background:#f4f7fb;white-space:pre-wrap;overflow-wrap:anywhere;border-left:3px solid #b9c9de}.log-entry.warning{background:#fff6e6;border-left-color:#d0a354}.log-entry.error{background:#fff0ed;border-left-color:#d68370}.log-time{color:var(--muted);margin-right:7px}.threshold{font-size:12px;font-weight:600;color:#825b20;margin:9px 0}.export-section{padding-top:13px!important}.collapse-summary{font-size:10px;color:#bbd3fa;margin-left:7px;white-space:nowrap}.bank-counts{font-size:11px;color:var(--muted);margin:8px 0}.topic-row{display:flex;gap:6px;margin-top:9px}.topic-row input{flex:1;min-width:0}.theme-associations{font-size:11px;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--muted);margin:8px 0}.tab-pane .switches{margin-bottom:12px}
@media(max-width:600px){.panel{width:calc(100vw - 24px)}.workspace{flex-direction:column;height:480px;max-height:calc(100vh - 244px);max-height:calc(100dvh - 244px)}.navigation{flex-direction:row;width:100%;border-right:0;border-bottom:1px solid var(--line);padding:7px;overflow-x:auto;flex-shrink:0}.nav-tab{padding:8px 9px}.panes{min-height:0}.tab-pane{padding:11px}.global-bar{padding:9px 11px}.brand{font-size:13px}.collapsed .header{gap:8px}.global-top{gap:5px}.global-action{padding:5px 7px}.setting-row{grid-template-columns:80px minmax(0,1fr)}}
`;
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');
header.title = '拖动标题栏移动面板';
const brand = el(doc, 'span', 'brand', '谊安学助手 2.0');
const collapseSummary = el(doc, 'span', 'collapse-summary');
const expand = el(doc, 'button', 'toggle', '展开');
expand.type = 'button';
expand.setAttribute('aria-label', '展开题库助手');
expand.setAttribute('aria-expanded', 'false');
header.append(brand, collapseSummary, expand);
const body = el(doc, 'div', 'body');
body.hidden = true;
panel.append(header, body);
root.appendChild(panel);
const globalBar = el(doc, 'div', 'global-bar');
const globalTop = el(doc, 'div', 'global-top');
const globalStatus = el(doc, 'span', 'global-status', '尚未开始');
globalStatus.setAttribute('data-testid', 'learning-status');
globalStatus.setAttribute('aria-live', 'polite');
const globalPause = el(doc, 'button', 'global-action', '暂停全部');
globalPause.type = 'button';
globalPause.setAttribute('data-testid', 'global-pause');
globalPause.addEventListener('click', function () { invoke('onTaskAction', ['paused', 'stopped'].includes(state.learning.status) ? 'resume' : 'pause'); });
globalTop.append(globalStatus, globalPause);
const globalProgressRow = el(doc, 'div', 'global-progress-row');
const globalProgress = el(doc, 'progress', 'global-progress');
globalProgress.max = 1; globalProgress.value = 0;
globalProgress.setAttribute('aria-label', '已完成任务进度');
const globalProgressLabel = el(doc, 'span', 'global-progress-label', '0 / 0');
const workerStatus = el(doc, 'div', 'worker-status');
globalProgressRow.append(globalProgress, globalProgressLabel);
globalBar.append(globalTop, globalProgressRow, workerStatus);
const workspace = el(doc, 'div', 'workspace');
const navigation = el(doc, 'nav', 'navigation');
navigation.setAttribute('role', 'tablist');
navigation.setAttribute('aria-label', '助手功能');
const panes = el(doc, 'div', 'panes');
const tabs = {}, views = {};
const tabOrder = ['overview', 'video', 'exam', 'bank', 'console', 'settings'];
const tabNames = { overview: '任务总览', video: '视频学习', exam: '考试答题', bank: '题库搜索', console: '控制台', settings: '其他设置' };
function selectTab(key, focus) {
if (!views[key]) return;
state.activeTab = key;
tabOrder.forEach(function (id) {
const active = id === key;
tabs[id].setAttribute('aria-selected', String(active));
tabs[id].tabIndex = active ? 0 : -1;
views[id].hidden = !active;
});
if (focus) tabs[key].focus();
scheduleClamp(true);
}
tabOrder.forEach(function (key, index) {
const tab = el(doc, 'button', 'nav-tab', tabNames[key]);
tab.type = 'button'; tab.id = 'yian-tab-' + key;
tab.setAttribute('role', 'tab'); tab.setAttribute('aria-controls', 'yian-pane-' + key);
tab.setAttribute('data-testid', 'tab-' + key);
tab.addEventListener('click', function () { selectTab(key, false); });
tab.addEventListener('keydown', function (event) {
let target;
if (event.key === 'Home') target = tabOrder[0];
if (event.key === 'End') target = tabOrder[tabOrder.length - 1];
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') target = tabOrder[(index + 1) % tabOrder.length];
if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') target = tabOrder[(index + tabOrder.length - 1) % tabOrder.length];
if (target) { event.preventDefault(); selectTab(target, true); }
});
const view = el(doc, 'section', 'tab-pane');
view.id = 'yian-pane-' + key; view.setAttribute('role', 'tabpanel');
view.setAttribute('aria-labelledby', tab.id); view.setAttribute('data-testid', 'pane-' + key);
tabs[key] = tab; views[key] = view;
navigation.appendChild(tab); panes.appendChild(view);
});
workspace.append(navigation, panes);
body.append(globalBar, workspace);
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.addEventListener('click', togglePanel);
function positionValue(value) {
if (!value || !Number.isFinite(value.left) || !Number.isFinite(value.top)) return null;
return { left: Math.round(value.left), top: Math.round(value.top) };
}
function positionSignature(value) { return value ? value.left + ',' + value.top : 'default'; }
function clampPosition(value) {
const viewport = global.visualViewport;
const left = viewport ? viewport.offsetLeft : 0;
const top = viewport ? viewport.offsetTop : 0;
const width = viewport ? viewport.width : global.innerWidth;
const height = viewport ? viewport.height : global.innerHeight;
const rect = host.getBoundingClientRect();
// SPA 暂时隐藏面板时不依据 0×0 尺寸移动或覆盖已保存的位置。
if (rect.width <= 0 || rect.height <= 0) return { left: value.left, top: value.top };
return {
left: Math.round(Math.max(left, Math.min(value.left, left + Math.max(0, width - rect.width)))),
top: Math.round(Math.max(top, Math.min(value.top, top + Math.max(0, height - rect.height))))
};
}
function applyPosition(value) {
const next = value ? clampPosition(value) : null;
const signature = positionSignature(next);
const changed = positionSignature(state.position) !== signature;
state.position = next;
if (signature === appliedPosition) return changed;
appliedPosition = signature;
if (next) {
host.style.setProperty('left', next.left + 'px', 'important');
host.style.setProperty('top', next.top + 'px', 'important');
host.style.setProperty('right', 'auto', 'important');
host.style.setProperty('bottom', 'auto', 'important');
} else {
host.style.removeProperty('left'); host.style.removeProperty('top');
host.style.setProperty('right', '16px', 'important');
host.style.setProperty('bottom', '16px', 'important');
}
return changed;
}
function savePosition() {
if (state.position) invoke('onPositionChange', { left: state.position.left, top: state.position.top });
}
function scheduleClamp(notify) {
if (destroyed) return;
notifyClamp = notifyClamp || Boolean(notify);
if (clampFrame !== null) return;
clampFrame = global.requestAnimationFrame(function () {
clampFrame = null;
const shouldNotify = notifyClamp;
notifyClamp = false;
if (destroyed || !state.position) return;
const changed = applyPosition(state.position);
if (changed && shouldNotify && !drag) savePosition();
});
}
function removeDragListeners() {
global.removeEventListener('pointermove', moveDrag, true);
global.removeEventListener('pointerup', finishDrag, true);
global.removeEventListener('pointercancel', finishDrag, true);
doc.removeEventListener('selectstart', preventDragSelection, true);
}
function preventDragSelection(event) { if (drag && drag.moved && event.cancelable) event.preventDefault(); }
function startDrag(event) {
suppressHeaderClick = false;
if (destroyed || drag || event.isPrimary === false || event.button !== 0) return;
const target = event.target && event.target.nodeType === 1 ? event.target : null;
if (!target || target.closest('button,input,select,textarea,a,[contenteditable="true"],[role="button"]')) return;
const rect = host.getBoundingClientRect();
drag = { id: event.pointerId, x: event.clientX, y: event.clientY, left: rect.left, top: rect.top, moved: false, captured: false };
global.addEventListener('pointermove', moveDrag, { capture: true, passive: false });
global.addEventListener('pointerup', finishDrag, true);
global.addEventListener('pointercancel', finishDrag, true);
doc.addEventListener('selectstart', preventDragSelection, true);
}
function moveDrag(event) {
if (!drag || event.pointerId !== drag.id) return;
const deltaX = event.clientX - drag.x, deltaY = event.clientY - drag.y;
if (!drag.moved && Math.hypot(deltaX, deltaY) <= 4) return;
if (!drag.moved) {
drag.moved = true;
panel.classList.add('dragging');
try { header.setPointerCapture(drag.id); drag.captured = true; } catch (_) { /* 全局监听仍可完成拖动。 */ }
}
if (event.cancelable) event.preventDefault();
applyPosition({ left: drag.left + deltaX, top: drag.top + deltaY });
}
function finishDrag(event) {
if (!drag || event.pointerId !== drag.id) return;
const ended = drag;
drag = null;
removeDragListeners();
panel.classList.remove('dragging');
if (ended.captured) {
try { if (header.hasPointerCapture(ended.id)) header.releasePointerCapture(ended.id); } catch (_) { /* 浏览器取消时可能已自动释放。 */ }
}
if (ended.moved) {
suppressHeaderClick = true;
if (event.cancelable) event.preventDefault();
applyPosition(state.position);
savePosition();
}
}
function suppressDragClick(event) {
if (!suppressHeaderClick || event.detail === 0) return;
suppressHeaderClick = false;
event.preventDefault(); event.stopImmediatePropagation();
}
function viewportChanged() { scheduleClamp(true); }
header.addEventListener('pointerdown', startDrag);
header.addEventListener('lostpointercapture', finishDrag);
header.addEventListener('click', suppressDragClick, true);
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);
views.exam.appendChild(switches);
views.settings.appendChild(copySwitch.label);
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);
views.bank.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 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');
body.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);
views.bank.appendChild(searchSection);
function search(offset) {
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 (!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);
views.exam.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);
views.settings.appendChild(exportSection);
const learningControls = {};
function learningSetting(container, key, label, values, fallback, range) {
const row = el(doc, 'label', 'setting-row');
const input = el(doc, values ? 'select' : 'input');
input.setAttribute('aria-label', label);
input.setAttribute('data-testid', 'setting-' + key);
if (values) values.forEach(function (entry) {
const option = el(doc, 'option', '', entry[1]); option.value = plain(entry[0]); input.appendChild(option);
});
else { input.type = 'number'; input.min = plain(range && range[0] || 0); input.max = plain(range && range[1] || 3600); input.step = plain(range && range[2] || 1); }
row.append(el(doc, 'span', '', label), input); container.appendChild(row);
learningControls[key] = { input: input, fallback: fallback, numeric: !values || typeof fallback === 'number' };
input.addEventListener('change', function () {
if (!input.checkValidity()) { setNotice('请输入有效的' + label + '。', 'warning'); return; }
const value = learningControls[key].numeric ? Number(input.value) : input.value;
state.learning[key] = value;
invoke('onLearningSetting', key, value);
refreshLearning();
});
return input;
}
function taskButton(container, label, action, className) {
const button = el(doc, 'button', className || '', label); button.type = 'button';
button.setAttribute('data-testid', 'task-' + action);
button.addEventListener('click', function () { invoke('onTaskAction', action); });
container.appendChild(button); return button;
}
views.overview.appendChild(el(doc, 'h2', 'section-title', '本轮学习任务'));
learningSetting(views.overview, 'mode', '任务模式', [['video', '只刷视频(课中题人工处理)'], ['videoQuiz', '视频与课中答题'], ['exam', '只做考试'], ['all', '视频、课中题与考试']], 'all');
learningSetting(views.overview, 'execution', '执行方式', [['sequential', '顺序执行'], ['parallel', '并行:一个视频 + 一场考试']], 'sequential');
const taskActions = el(doc, 'div', 'task-actions');
const startTask = taskButton(taskActions, '开始本轮', 'start', 'primary');
const resumeTask = taskButton(taskActions, '继续上次任务', 'resume');
const stopTask = taskButton(taskActions, '停止本轮', 'stop');
views.overview.appendChild(taskActions);
const learningMessage = el(doc, 'p', 'explanation');
const summaryGrid = el(doc, 'div', 'summary-grid');
const summaryNodes = {};
[['total', '任务总数'], ['completed', '已完成'], ['skipped', '已跳过'], ['waiting', '等待人工'], ['failed', '失败'], ['etaSeconds', '预计剩余']].forEach(function (entry) {
const card = el(doc, 'div', 'summary-card'); const value = el(doc, 'strong', '', '0');
card.append(value, el(doc, 'span', '', entry[1])); summaryNodes[entry[0]] = value; summaryGrid.appendChild(card);
});
const videoSummary = el(doc, 'button', 'worker-card'); videoSummary.type = 'button';
const examSummary = el(doc, 'button', 'worker-card'); examSummary.type = 'button';
videoSummary.addEventListener('click', function () { selectTab('video', true); });
examSummary.addEventListener('click', function () { selectTab('exam', true); });
const taskList = el(doc, 'div', 'task-list');
views.overview.append(learningMessage, summaryGrid, videoSummary, examSummary, el(doc, 'h3', 'section-title', '任务清单'), taskList);
views.video.appendChild(el(doc, 'h2', 'section-title', '视频学习'));
const videoModeNote = el(doc, 'p', 'mode-note'); views.video.appendChild(videoModeNote);
learningSetting(views.video, 'rate', '播放倍速', [[1, '1X'], [1.25, '1.25X'], [1.5, '1.5X'], [2, '2X(最高)']], 1);
const videoInfo = el(doc, 'dl', 'info-grid');
const videoActions = el(doc, 'div', 'task-actions');
const videoPause = taskButton(videoActions, '暂停视频任务', 'videoPause');
const videoResume = taskButton(videoActions, '继续视频任务', 'videoResume');
const videoMessage = el(doc, 'p', 'explanation');
views.video.append(videoInfo, videoActions, videoMessage, el(doc, 'p', 'hint', '播放结束后等待平台确认完成;倍速不会保证平台要求的学习时长按比例缩短。课中题出现时暂停播放,精确匹配后处理。'));
const examIntro = el(doc, 'div', 'section');
examIntro.appendChild(el(doc, 'h2', 'section-title', '考试设置与进度'));
const examModeNote = el(doc, 'p', 'mode-note'); examIntro.appendChild(examModeNote);
learningSetting(examIntro, 'answerInterval', '答题间隔(秒)', null, 4, [0, 120, 0.5]);
learningSetting(examIntro, 'minSubmitMinutes', '最早交卷(分)', null, 6, [0.5, 180, 0.5]);
const examInfo = el(doc, 'dl', 'info-grid');
const examMessage = el(doc, 'p', 'explanation');
const examActions = el(doc, 'div', 'task-actions');
const examPause = taskButton(examActions, '暂停考试任务', 'examPause');
const examResume = taskButton(examActions, '继续考试任务', 'examResume');
examIntro.append(examInfo, el(doc, 'p', 'threshold', '预计得分必须严格大于 80 分;正好 80 分等待人工。'), examMessage, examActions,
el(doc, 'p', 'hint', '仅已精确匹配、点击并核验完整答案文字的题目计分。进入原试卷满设定时长、核验完成且满足条件后才自动交卷;预计分数以本地题库为依据。练习与课中题不使用考试交卷门槛。'));
views.exam.insertBefore(examIntro, switches);
const bankCounts = el(doc, 'p', 'bank-counts'); bankSection.appendChild(bankCounts);
const associationSection = el(doc, 'details', 'page-section');
associationSection.appendChild(el(doc, 'summary', 'section-title', '课程与考试主题关联'));
const associationTitle = el(doc, 'input'); associationTitle.type = 'text';
associationTitle.placeholder = '课程或考试的完整标题'; associationTitle.setAttribute('aria-label', '关联的完整标题');
const associationCourses = el(doc, 'textarea');
associationCourses.placeholder = '填写题库中的完整课程名,多个课程用换行或逗号分隔';
associationCourses.setAttribute('aria-label', '关联题库课程');
associationCourses.style.cssText = 'font:inherit;width:100%;min-height:70px;margin-top:7px;padding:8px;border:1px solid #cfd8e7;border-radius:7px;resize:vertical;color:#17253d;background:white;';
const associationActions = el(doc, 'div', 'task-actions');
const saveAssociation = el(doc, 'button', '', '保存关联'); saveAssociation.type = 'button';
const deleteAssociation = el(doc, 'button', '', '删除关联'); deleteAssociation.type = 'button';
associationActions.append(saveAssociation, deleteAssociation);
const associationList = el(doc, 'p', 'theme-associations');
associationSection.append(associationTitle, associationCourses, associationActions,
el(doc, 'p', 'hint', '填写精确标题和题库课程名称。相似关键词只能帮助查找,不能授权自动答题。'), associationList);
views.bank.appendChild(associationSection);
let associationSignature = '';
associationTitle.addEventListener('change', function () {
associationCourses.value = ((state.learning.associations || {})[normalized(associationTitle.value)] || []).join('\n');
});
saveAssociation.addEventListener('click', function () {
const title = normalized(associationTitle.value);
const courses = Array.from(new Set(associationCourses.value.split(/[\n,,]+/).map(normalized).filter(Boolean)));
if (!title || !courses.length) { setNotice('请填写完整标题及至少一个完整题库课程名。', 'warning'); return; }
const knownCourses = (state.courses || []).map(function (course) { return typeof course === 'string' ? course : plain(course.value == null ? course.course || course.name : course.value); });
const missing = courses.filter(function (course) { return !knownCourses.includes(course); });
if (missing.length) { setNotice('题库中没有找到这些完整课程名:' + missing.join('、'), 'warning'); return; }
const associations = Object.assign({}, state.learning.associations || {});
associations[title] = courses; state.learning.associations = associations;
invoke('onLearningSetting', 'associations', associations); refreshLearning(); setNotice('主题关联已保存,将重新检查受影响的答题任务。', 'success');
});
deleteAssociation.addEventListener('click', function () {
const title = normalized(associationTitle.value);
if (!title || !Object.prototype.hasOwnProperty.call(state.learning.associations || {}, title)) { setNotice('该完整标题尚未保存关联。', 'warning'); return; }
const associations = Object.assign({}, state.learning.associations || {});
delete associations[title]; state.learning.associations = associations; associationCourses.value = '';
invoke('onLearningSetting', 'associations', associations); refreshLearning(); setNotice('主题关联已删除。', 'success');
});
views.console.appendChild(el(doc, 'h2', 'section-title', '运行控制台'));
const logActions = el(doc, 'div', 'console-actions');
const logFilter = el(doc, 'select', 'console-filter'); logFilter.setAttribute('aria-label', '日志级别');
[['all', '全部日志'], ['info', '运行状态'], ['warning', '警告'], ['error', '错误']].forEach(function (entry) { const option = el(doc, 'option', '', entry[1]); option.value = entry[0]; logFilter.appendChild(option); });
const copyLog = el(doc, 'button', '', '复制日志'); copyLog.type = 'button'; copyLog.addEventListener('click', function () { invoke('onLogAction', 'copy'); });
const clearLog = el(doc, 'button', '', '清空显示'); clearLog.type = 'button'; clearLog.addEventListener('click', function () { invoke('onLogAction', 'clear'); });
logActions.append(logFilter, copyLog, clearLog);
const logCount = el(doc, 'p', 'hint'); const logList = el(doc, 'div', 'log-list');
logList.setAttribute('data-testid', 'learning-logs');
views.console.append(logActions, logCount, logList);
logFilter.addEventListener('change', function () { refreshLearning(); });
const recoverySection = el(doc, 'div', 'page-section');
recoverySection.appendChild(el(doc, 'h2', 'section-title', '异常恢复'));
learningSetting(recoverySection, 'stallSeconds', '停滞检查(秒)', null, 60, [5, 600, 1]);
learningSetting(recoverySection, 'retryDelaySeconds', '续播等待(秒)', null, 15, [1, 120, 1]);
learningSetting(recoverySection, 'maxRefresh', '最多自动刷新', null, 3, [0, 10, 1]);
recoverySection.appendChild(el(doc, 'p', 'hint', '主动暂停、课中题、等待平台确认、等待交卷和等待人工不会触发故障刷新。两次刷新至少间隔 30 秒。'));
views.settings.append(recoverySection, el(doc, 'p', 'explanation', '切换标签页或最小化后尽量继续运行,回到前台会按网页实际状态核对。浏览器关闭、设备休眠或标签冻结期间无法保证继续;不会虚增平台学习时长。'));
function autoUnavailableReason() {
if (!(Number(state.bankTotal) > 0)) return '请先在题库搜索栏导入统一题库。';
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 || '请人工核对。'));
});
}
const learningStatusNames = { idle: '尚未开始', ready: '等待开始', running: '运行中', loading: '加载中', scanning: '识别任务中', video: '视频学习中', playing: '播放中', quiz: '课中答题', exam: '考试答题中', answering: '答题中', preflight: '题库预检', waiting: '等待人工', waitingHuman: '等待人工', waitingSubmit: '等待交卷', waitingPlatform: '等待平台确认', paused: '已暂停', stopped: '已停止', completed: '已完成', done: '已完成', skipped: '已跳过', failed: '失败', error: '异常', retrying: '恢复中', pending: '待处理', submitted: '已交卷', finished: '本轮结束', unknown: '结果待确认', submitting: '提交中', confirming: '确认交卷中', expanded: '已读取目录' };
function statusName(value) { return value === 'awaitingAttempt' ? '等待确认新场次' : learningStatusNames[plain(value)] || plain(value) || '待处理'; }
function firstValue(object, keys, fallback) {
object = object || {};
for (let i = 0; i < keys.length; i += 1) if (object[keys[i]] != null && object[keys[i]] !== '') return object[keys[i]];
return fallback;
}
function duration(value) {
if (value == null || value === '' || !Number.isFinite(Number(value))) return '待确认';
const seconds = Math.max(0, Math.ceil(Number(value)));
return (seconds >= 3600 ? Math.floor(seconds / 3600) + ':' : '') + String(Math.floor(seconds / 60) % 60).padStart(2, '0') + ':' + String(seconds % 60).padStart(2, '0');
}
function infoRows(container, entries) {
container.replaceChildren();
entries.forEach(function (entry) { container.append(el(doc, 'dt', '', entry[0]), el(doc, 'dd', '', entry[1])); });
}
let taskSignature = '', logSignature = '';
function refreshLearning() {
const learning = state.learning || {};
const video = learning.video || {}, exam = learning.exam || {};
const total = Math.max(0, Number(learning.total) || 0), completed = Math.max(0, Number(learning.completed) || 0);
const paused = ['paused', 'stopped'].includes(learning.status);
const active = !['idle', 'ready', 'stopped', 'completed', 'done', 'finished'].includes(learning.status || 'idle');
setText(globalStatus, statusName(learning.status) + (Number(learning.waiting) > 0 ? ' · ' + Number(learning.waiting) + ' 项等待人工' : ''));
setText(globalPause, paused ? '继续全部' : '暂停全部');
globalPause.disabled = !active && !paused;
globalProgress.max = Math.max(total, 1); globalProgress.value = Math.min(completed, total);
setText(globalProgressLabel, completed + ' / ' + total + ' 已完成');
const videoTitle = plain(firstValue(video, ['title', 'course', 'name'], '暂无视频任务'));
const examTitle = plain(firstValue(exam, ['title', 'theme', 'name'], '暂无考试任务'));
const videoState = statusName(video.status || 'idle'), examState = statusName(exam.status || 'idle');
setText(workerStatus, '视频:' + videoState + ' 考试:' + examState + (learning.message ? '\n' + plain(learning.message) : ''));
setText(collapseSummary, completed + '/' + total + (Number(learning.waiting) > 0 ? ' · 待人工' : paused ? ' · 已暂停' : ''));
collapseSummary.hidden = Boolean(state.expanded);
setText(learningMessage, learning.message || '在学习计划、课程或考试页面开始本轮,先读取实际任务,再按网页状态执行。人工刷新后需继续上次任务。');
Object.keys(summaryNodes).forEach(function (key) { setText(summaryNodes[key], key === 'etaSeconds' ? duration(learning.etaSeconds) : Math.max(0, Number(learning[key]) || 0)); });
startTask.disabled = active;
resumeTask.disabled = !paused && !['waiting', 'waitingHuman'].includes(learning.status);
stopTask.disabled = !active && !paused;
setText(videoSummary, '视频 · ' + videoState + '\n' + videoTitle);
setText(examSummary, '考试 · ' + examState + '\n' + examTitle);
const tasks = Array.isArray(learning.tasks) ? learning.tasks : [];
const taskNextSignature = JSON.stringify(tasks);
if (taskNextSignature !== taskSignature || !taskList.childElementCount) {
taskSignature = taskNextSignature; taskList.replaceChildren();
if (!tasks.length) taskList.appendChild(el(doc, 'p', 'empty', '尚未读取到任务。完成状态以平台记录为准,跳过项目不会计为已完成。'));
tasks.forEach(function (task) {
const row = el(doc, 'article', 'task-row');
row.append(el(doc, 'p', 'task-name', firstValue(task, ['title', 'name', 'course'], '未命名任务')),
el(doc, 'div', 'task-state', ([(task.kind || task.type) === 'video' ? '视频' : (task.kind || task.type) === 'exam' ? '考试' : (task.kind || task.type) === 'course' ? '课程' : task.kind || task.type, statusName(task.status), task.message || task.reason].filter(Boolean)).join(' · ')));
taskList.appendChild(row);
});
}
Object.keys(learningControls).forEach(function (key) {
const setting = learningControls[key];
if (root.activeElement !== setting.input) setting.input.value = plain(learning[key] == null ? setting.fallback : learning[key]);
});
const videoEnabled = learning.mode !== 'exam';
setText(videoModeNote, videoEnabled ? '' : '本模式未启用视频;可查看设置与记录。'); videoModeNote.hidden = videoEnabled;
const examEnabled = learning.mode === 'exam' || learning.mode === 'all' || !learning.mode;
setText(examModeNote, examEnabled ? '' : '本模式未启用独立考试;仍可操作当前练习页。'); examModeNote.hidden = examEnabled;
const videoDuration = firstValue(video, ['duration', 'durationSeconds', 'totalSeconds'], null);
const learnedTime = firstValue(video, ['learnedTime', 'learnedSeconds', 'platformSeconds'], null);
infoRows(videoInfo, [['当前课程', videoTitle], ['播放进度', duration(firstValue(video, ['currentTime', 'position', 'positionSeconds'], null)) + ' / ' + duration(videoDuration)],
['平台记录', duration(learnedTime)], ['平台完成', video.isLearned === true || video.completed === true ? '已确认完成' : '尚未确认'],
['实际倍速', firstValue(video, ['actualRate', 'playbackRate', 'rate'], null) == null ? '等待播放器' : plain(firstValue(video, ['actualRate', 'playbackRate', 'rate'], 1)) + 'X'],
['课中题', firstValue(video, ['quizStatus', 'quizMessage'], '未发现弹题')], ['恢复次数', plain(firstValue(video, ['refreshCount', 'retries', 'retryCount'], 0))]]);
setText(videoMessage, video.message || video.reason || '等待视频任务。播放结束与平台确认完成分别记录。');
const videoHasTask = Boolean(video.title || video.name || video.course || video.taskId || video.id);
videoPause.disabled = !videoHasTask || ['paused', 'completed', 'done', 'idle'].includes(video.status || 'idle');
videoResume.disabled = !videoHasTask || !['paused', 'waiting', 'waitingHuman', 'failed', 'error'].includes(video.status);
infoRows(examInfo, [['考试主题', examTitle], ['题库预检', plain(firstValue(exam, ['preflightMessage', 'themeStatus', 'preflight'], '尚未预检'))],
['预计得分', firstValue(exam, ['predictedScore', 'score'], null) == null ? '待核算' : plain(firstValue(exam, ['predictedScore', 'score'], 0)) + ' 分(题库估计)'],
['网页剩余', duration(firstValue(exam, ['remainingSeconds', 'remainingTime'], null))], ['进入试卷', duration(firstValue(exam, ['elapsedSeconds', 'elapsedTime'], null))],
['最早交卷', duration(firstValue(exam, ['submitWaitSeconds', 'waitSeconds'], null))], ['交卷状态', statusName(exam.status || 'idle')]]);
setText(examMessage, exam.message || exam.reason || '先确认主题与题库对应。没有对应题库则跳过;不明确的答案等待人工。');
const examHasTask = Boolean(exam.title || exam.name || exam.theme || exam.taskId || exam.id);
examPause.disabled = !examHasTask || ['paused', 'submitted', 'completed', 'done', 'idle'].includes(exam.status || 'idle');
examResume.disabled = !examHasTask || !['paused', 'waiting', 'waitingHuman', 'failed', 'error'].includes(exam.status);
tabs.video.setAttribute('data-alert', String(['waiting', 'waitingHuman', 'failed', 'error'].includes(video.status)));
tabs.exam.setAttribute('data-alert', String(['waiting', 'waitingHuman', 'failed', 'error'].includes(exam.status)));
const associations = learning.associations && typeof learning.associations === 'object' ? learning.associations : {};
const nextAssociationSignature = JSON.stringify(associations);
if (!associationTitle.value && learning.currentTitle && root.activeElement !== associationTitle) associationTitle.value = plain(learning.currentTitle);
if (associationSignature !== nextAssociationSignature) {
associationSignature = nextAssociationSignature;
setText(associationList, Object.keys(associations).map(function (title) { return title + ' → ' + (Array.isArray(associations[title]) ? associations[title] : []).join('、'); }).join('\n') || '暂无手工主题关联。');
if (root.activeElement !== associationCourses) associationCourses.value = (associations[normalized(associationTitle.value)] || []).join('\n');
}
const logs = (Array.isArray(learning.logs) ? learning.logs : []).slice(-500);
const filtered = logs.filter(function (entry) { return logFilter.value === 'all' || (entry.level === 'warn' ? 'warning' : entry.level || 'info') === logFilter.value; });
const nextLogSignature = JSON.stringify([logFilter.value, logs]);
if (nextLogSignature !== logSignature || !logList.childElementCount) {
logSignature = nextLogSignature; logList.replaceChildren();
if (!filtered.length) logList.appendChild(el(doc, 'p', 'empty', logs.length ? '该级别暂无日志。' : '暂无日志。清空显示不会删除任务检查点。'));
filtered.forEach(function (entry) {
const level = entry.level === 'warn' ? 'warning' : entry.level || 'info';
const row = el(doc, 'div', 'log-entry ' + level);
let time = plain(entry.time || '');
if (typeof entry.time === 'number' || /^\d{4}-\d{2}-\d{2}T/.test(time)) { const date = new Date(entry.time); if (!Number.isNaN(date.getTime())) time = date.toLocaleTimeString('zh-CN', { hour12: false }); }
row.append(el(doc, 'span', 'log-time', time), doc.createTextNode((entry.task ? '[' + plain(entry.task) + '] ' : '') + plain(entry.message)));
logList.appendChild(row);
});
}
setText(logCount, '显示 ' + filtered.length + ' 条 / 最近 ' + logs.length + ' 条;最多保留 500 条。');
}
let courseSignature = '';
function refresh() {
if (destroyed) return;
panel.classList.toggle('collapsed', !state.expanded);
body.hidden = !state.expanded;
brand.textContent = '谊安学助手 2.0';
expand.textContent = state.expanded ? '收起' : '展开';
expand.setAttribute('aria-label', state.expanded ? '收起题库助手' : '展开题库助手');
expand.setAttribute('aria-expanded', String(Boolean(state.expanded)));
if (previousExpanded !== Boolean(state.expanded)) {
previousExpanded = Boolean(state.expanded);
scheduleClamp(true);
}
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') + ' 条来源记录';
bankCounts.textContent = '题目 ' + Number(state.uniqueTotal || 0).toLocaleString('zh-CN') + ' 道 · 答案冲突 ' + Number(state.conflictTotal || 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;
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 = 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);
refreshLearning();
selectTab(views[state.activeTab] ? state.activeTab : 'overview', false);
}
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 = '统一题库 · 找到 ' + resultTotal.toLocaleString('zh-CN') + ' 道题';
results.replaceChildren();
if (!items.length) results.appendChild(el(doc, 'div', 'empty', Number(state.bankTotal) > 0 ? '未找到题目,可调整关键词或课程筛选。' : '导入统一题库后,输入题干、关键词或选项文字查找答案。'));
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;
const update = Object.assign({}, partial || {});
let incoming = null, acceptPosition = false;
if (Object.prototype.hasOwnProperty.call(update, 'position')) {
incoming = positionValue(update.position);
const signature = positionSignature(incoming);
acceptPosition = signature !== lastPositionInput && !drag;
lastPositionInput = signature;
delete update.position;
}
if (Object.prototype.hasOwnProperty.call(update, 'learning')) {
update.learning = Object.assign({}, state.learning || {}, update.learning || {});
}
Object.assign(state, update);
refresh();
if (acceptPosition) {
applyPosition(incoming);
if (incoming && positionSignature(incoming) !== positionSignature(state.position)) savePosition();
}
}
function destroy() {
if (destroyed) return;
destroyed = true;
const oldDrag = drag;
drag = null;
removeDragListeners();
if (oldDrag && oldDrag.captured) {
try { if (header.hasPointerCapture(oldDrag.id)) header.releasePointerCapture(oldDrag.id); } catch (_) { /* 节点卸载时浏览器可能已释放。 */ }
}
header.removeEventListener('pointerdown', startDrag);
header.removeEventListener('lostpointercapture', finishDrag);
header.removeEventListener('click', suppressDragClick, true);
expand.removeEventListener('click', togglePanel);
brand.removeEventListener('click', togglePanel);
global.removeEventListener('resize', viewportChanged);
if (global.visualViewport) {
global.visualViewport.removeEventListener('resize', viewportChanged);
global.visualViewport.removeEventListener('scroll', viewportChanged);
}
if (resizeObserver) resizeObserver.disconnect();
if (clampFrame !== null) global.cancelAnimationFrame(clampFrame);
clampFrame = null;
doc.removeEventListener('selectionchange', rememberSelection);
host.remove();
}
(doc.body || doc.documentElement).appendChild(host);
global.addEventListener('resize', viewportChanged);
if (global.visualViewport) {
global.visualViewport.addEventListener('resize', viewportChanged);
global.visualViewport.addEventListener('scroll', viewportChanged);
}
if (typeof global.ResizeObserver === 'function') {
let sizeSignature = '';
resizeObserver = new global.ResizeObserver(function (entries) {
const box = entries[0] && entries[0].contentRect;
if (!box) return;
const signature = box.width + ',' + box.height;
if (signature === sizeSignature) return;
sizeSignature = signature;
scheduleClamp(true);
});
resizeObserver.observe(panel);
}
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 = ['course', 'sourceUrl', 'routeKey', 'pageSignature', 'revision', 'exam', 'mode', 'topicKey'];
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.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 (item !== items[0]) {
const configured = typeof options.getInterval === 'function' ? Number(options.getInterval()) : 0;
const gap = Number.isFinite(configured) ? Math.max(0, Math.min(120, configured)) * 1000 : 4000;
if (gap && !await wait(run, gap)) return progress();
}
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);
/* YianLearningEngine: 读取网页实际目录、控制原生播放器并协调已授权的学习任务。 */
(function (root, factory) {
'use strict';
const api = factory(root);
if (typeof module === 'object' && module.exports) module.exports = api;
root.YianLearningEngine = api;
})(typeof window !== 'undefined' ? window : globalThis, function (root) {
'use strict';
const TERMINAL = ['completed', 'skipped', 'failed', 'expanded'];
const ACTIVE = ['loading', 'playing', 'quiz', 'answering', 'waitingSubmit', 'retrying'];
const MODES = ['video', 'videoQuiz', 'exam', 'all'];
const LEASE_MS = 18000;
const SESSION_KEY = 'session.v1';
const CONTROL_KEY = 'control.v1';
function clean(value) { return String(value == null ? '' : value).replace(/[\u200b-\u200d\ufeff]/g, '').replace(/\s+/g, ' ').trim(); }
function clone(value) { return JSON.parse(JSON.stringify(value)); }
function hash(value) { let h = 2166136261; for (const c of String(value)) { h ^= c.charCodeAt(0); h = Math.imul(h, 16777619); } return (h >>> 0).toString(36); }
function token() { return root.crypto && root.crypto.randomUUID ? root.crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2); }
function safeURL(value, base) {
try { const url = new URL(value, base); return url.origin === 'http://10.119.67.7' && url.pathname.startsWith('/clientcn/') ? url : null; } catch (_) { return null; }
}
function canonical(value) { try { const u = new URL(value); u.hash = ''; return u.href; } catch (_) { return String(value || ''); } }
function visible(node) {
if (!node || node.closest('[hidden],[aria-hidden="true"],.ant-modal-hidden')) return false;
for (let n = node; n && n.nodeType === 1; n = n.parentElement) {
if (n.style && (n.style.display === 'none' || n.style.visibility === 'hidden')) return false;
if (n.isConnected && n.ownerDocument.defaultView && n.ownerDocument.defaultView.getComputedStyle) {
const style = n.ownerDocument.defaultView.getComputedStyle(n);
if (style.display === 'none' || style.visibility === 'hidden') return false;
}
}
return true;
}
function textAt(doc, selectors) { for (const selector of selectors.split(',')) { const node = doc.querySelector(selector); if (node && clean(node.textContent)) return clean(node.textContent); } return ''; }
function number(value) { const n = Number(value); return Number.isFinite(n) && n >= 0 ? n : null; }
function seconds(value) {
const text = clean(value); let total = 0; let matched = false;
const clock = text.match(/(?:^|\s)(\d{1,3}):(\d{2})(?::(\d{2}))?(?:$|\s)/);
if (clock) return clock[3] == null ? Number(clock[1]) * 60 + Number(clock[2]) : Number(clock[1]) * 3600 + Number(clock[2]) * 60 + Number(clock[3]);
for (const entry of [[/(\d+(?:\.\d+)?)小时/, 3600], [/(\d+(?:\.\d+)?)分钟/, 60], [/(\d+(?:\.\d+)?)分(?!钟)/, 60], [/(\d+(?:\.\d+)?)秒/, 1]]) {
const m = text.match(entry[0]); if (m) { total += Number(m[1]) * entry[1]; matched = true; }
}
return matched ? total : null;
}
function nodeURL(node, url) {
const own = node.matches('[href],[data-task-url],[routerlink]') ? node : node.querySelector('a[href],[data-task-url],[routerlink]');
if (!own) return '';
const raw = own.getAttribute('href') || own.getAttribute('data-task-url') || own.getAttribute('routerlink');
if (!raw || /^(?:javascript:|#|\[|\{)/i.test(raw)) return '';
const resolved = safeURL(raw, url); return resolved ? resolved.href : '';
}
function isComplete(node) {
if (!node) return false;
if (node.matches('[data-is-learned="true"],[data-learned="true"],[data-completed="true"]')) return true;
if (node.querySelector('[data-is-learned="true"],[data-completed="true"]')) return true;
if (node.matches('.map_activity_content_info_completed') || node.querySelector('.map_activity_content_info_completed')) return true;
// 原站未完成图标也含 en_courseware_finished,必须排除 noFinished。
if (Array.from(node.querySelectorAll('.en_courseware_finished')).some(n => !n.classList.contains('en_courseware_noFinished'))) return true;
const explicit = node.querySelector('[data-learning-status],.courseware_finished_status,.learning-status');
return Boolean(explicit && /^(?:已完成|学习完成|已学完|完成)$/.test(clean(explicit.textContent)));
}
function titleOf(doc) {
const exact = textAt(doc, '[data-course-title],.course-courseware-other-top-left');
if (exact) return exact;
const header = doc.querySelector('.pc_course_left_top_header');
if (header) {
const copy = header.cloneNode(true); copy.querySelectorAll('.pc_course_left_top_header_info,.course-courseware,.course-courseware-playdefult').forEach(n => n.remove());
const title = clean(copy.textContent).replace(/(?:学习时长|发布时间|学习计划)[\s\S]*$/, '').trim(); if (title) return title;
}
const exam = doc.querySelector('.examdetail_header');
if (exam) return clean(exam.textContent).split(/已答|正确率|得分[::]/)[0].trim();
return textAt(doc, '[data-course-title],.pc_examtest_title,.top_map_title,h1') || clean(doc.title) || '未命名任务';
}
function quizOf(doc) {
const portal = Array.from(doc.querySelectorAll('.question-modal-pc .question_box,.question-modal-pc .answer_box')).find(visible);
if (portal) return portal.closest('.question-modal-pc');
return Array.from(doc.querySelectorAll('app-question-modal,[data-learning-quiz]')).find(node => visible(node) && Boolean(node.querySelector('.pc_examtest_content,[app-exam-question],input[type="radio"],input[type="checkbox"],[data-question],.question-content,.question_content') || clean(node.textContent))) || null;
}
function describePage(doc, url) {
const current = canonical(url || doc.URL);
const video = Array.from(doc.querySelectorAll('app-course-courseware-video video,app-video-base video,video[data-learning-video],video')).find(visible) || null;
const kind = doc.querySelector('app-examtest') ? 'paper' : video ? 'video' : doc.querySelector('app-examdetail') ? 'exam' : doc.querySelector('app-learning-map-activity-list') ? 'map' : doc.querySelector('app-course') ? 'course' : 'unknown';
const active = doc.querySelector('.cn_courseware_img.active,[data-courseware-id][aria-current="true"]');
const explicit = doc.querySelector('[data-account-id]');
return { kind, url: current, title: titleOf(doc), accountId: explicit ? clean(explicit.getAttribute('data-account-id')) : '',
coursewareTitle: active ? clean((active.querySelector('p') || active).textContent) : '',
video, activeCourseware: active, quiz: quizOf(doc), platformComplete: isComplete(active) || Boolean(doc.querySelector('app-course-courseware-video[data-is-learned="true"]')),
learnedSeconds: active ? seconds(clean((active.querySelector('.menu_courseware_left') || active).textContent)) : null,
requiredSeconds: number(active && active.getAttribute('data-required-seconds')),
hidden: doc.visibilityState === 'hidden' };
}
function discoverTasks(doc, url) {
const page = describePage(doc, url); const tasks = []; const seen = new Set();
function add(node, kind, selector, index, parentId) {
const explicitId = node.getAttribute('data-task-id') || node.getAttribute('data-courseware-id') || node.getAttribute('data-course-id') || node.getAttribute('data-exam-id') || node.id;
const targetURL = nodeURL(node, page.url);
let title = clean((node.querySelector('.activity_name,p,[data-task-title]') || node).textContent);
if (node.matches('.cn_courseware_img')) title = clean((node.querySelector(':scope > p') || node.querySelector('p') || node).textContent);
const identity = targetURL ? canonical(targetURL) : explicitId ? page.url + ':' + explicitId : page.url + ':' + selector + ':' + index;
const id = kind + ':' + hash(identity); if (seen.has(id)) return;
seen.add(id);
const locked = node.matches('[disabled],[aria-disabled="true"],[data-locked="true"],.locked') || Boolean(node.querySelector('[data-locked="true"]'));
tasks.push({ id, kind, title: title || kind, url: targetURL, pageUrl: page.url, selector, index, identity: explicitId || '', parentId: parentId || '',
status: isComplete(node) ? 'completed' : locked ? 'waiting' : 'pending', reason: locked ? '网站尚未开放此任务,等待前置条件' : '',
progress: isComplete(node) ? 1 : 0, duration: number(node.getAttribute('data-duration')), locked, retryCount: 0, lastRefreshAt: 0 });
return tasks[tasks.length - 1];
}
const mapNodes = doc.querySelectorAll('app-learning-map-activity-list .activity_content');
mapNodes.forEach((node, i) => {
const label = clean((node.querySelector('.acitvity_other_info') || node).textContent);
add(node, /考试/.test(label) ? 'exam' : 'course', 'app-learning-map-activity-list .activity_content', i);
});
const chapterNodes = doc.querySelectorAll('app-course .cn_courseware_img,[data-courseware-id]');
chapterNodes.forEach((node, i) => {
const icon = clean((node.querySelector('.cn_courseware_img_icon') || node).textContent);
const task = add(node, /视频/.test(icon) || node.getAttribute('data-kind') === 'video' ? 'video' : 'unsupported', 'app-course .cn_courseware_img,[data-courseware-id]', i);
if (task && task.kind === 'unsupported' && task.status !== 'completed') { task.status = 'skipped'; task.reason = '暂未适配此非视频课件'; }
node.querySelectorAll('.related_test,[data-related-exam]').forEach((exam, j) => {
const examTask = add(exam, 'exam', 'app-course .cn_courseware_img,[data-courseware-id]', i, task && task.id);
if (examTask) { examTask.selector = 'app-course .cn_courseware_img,[data-courseware-id]'; examTask.subSelector = '.related_test,[data-related-exam]'; examTask.subIndex = j; examTask.id += ':exam:' + j; examTask.requiresParent = exam.getAttribute('data-requires-complete') === 'true'; }
});
});
doc.querySelectorAll('a[href],[data-task-url]').forEach((node, i) => {
const kind = node.getAttribute('data-kind'); const u = nodeURL(node, page.url);
if (!u || node.closest('.activity_content,.cn_courseware_img,[data-courseware-id]')) return;
let inferred = /\/exam\//.test(u) ? 'exam' : /\/(?:course|courseware)\//.test(u) ? 'course' : '';
if (['video', 'exam', 'course'].includes(kind)) inferred = kind;
if (inferred) add(node, inferred, 'a[href],[data-task-url]', i);
});
if ((page.kind === 'exam' || page.kind === 'paper') && !tasks.some(t => t.kind === 'exam' && canonical(t.url || t.pageUrl) === page.url)) {
tasks.unshift({ id: 'exam:' + hash(page.url), kind: 'exam', title: page.title, url: page.url, pageUrl: page.url, current: true, status: 'pending', progress: 0, retryCount: 0 });
}
if (page.video && !tasks.some(t => t.kind === 'video' && doc.querySelectorAll(t.selector || ':not(*)')[t.index] === page.activeCourseware)) {
tasks.unshift({ id: 'video:' + hash(page.url + ':' + (page.video.id || 'player')), kind: 'video', title: page.coursewareTitle || page.title, url: page.url, pageUrl: page.url, current: true,
status: page.platformComplete ? 'completed' : 'pending', progress: page.platformComplete ? 1 : 0, retryCount: 0, lastRefreshAt: 0 });
}
return tasks;
}
function preflightTopic(doc, snapshot, associations, task, explicitTask) {
// 兼容控制器公开调用 (document, url, snapshot, associations, task)。
if (typeof snapshot === 'string') { snapshot = associations; associations = task; task = explicitTask; }
snapshot = snapshot || {}; associations = associations || {};
const title = clean(task && task.title || titleOf(doc));
const records = snapshot.records || snapshot.bank && snapshot.bank.records || [];
const declared = snapshot.courses || snapshot.themes || snapshot.bank && snapshot.bank.courses || [];
const available = Array.from(new Set((declared.length ? declared : records).map(c => clean(typeof c === 'string' ? c : c.course || c.name)).filter(Boolean)));
const explicit = associations[task && task.id] || associations[title];
const list = (Array.isArray(explicit) ? explicit : explicit ? [explicit] : []).map(clean).filter(Boolean);
if (list.length) {
const missing = list.filter(c => !available.includes(c));
return missing.length ? { status: 'missing', title, courses: [], candidates: available, reason: '明确关联的题库主题不存在:' + missing.join('、') } : { status: 'matched', title, courses: list, candidates: list, reason: '使用用户明确确认的主题关联' };
}
if (!available.length) return { status: 'missing', title, courses: [], candidates: [], reason: '统一题库为空或没有可用课程/主题' };
const exact = available.filter(c => c === title || c === clean(snapshot.course));
if (exact.length) return { status: 'matched', title, courses: exact, candidates: exact, reason: '考试主题与题库课程完全对应' };
const candidates = available.filter(c => c.length >= 2 && (title.includes(c) || c.includes(title))).slice(0, 20);
return { status: candidates.length ? 'waiting' : 'missing', title, courses: [], candidates, reason: candidates.length ? '仅发现相似主题,请先明确关联,暂不自动开考' : '未发现对应题库主题,已跳过自动开考;可在题库栏建立明确关联' };
}
function create(options) {
options = options || {};
const now = options.now || Date.now;
const storage = options.storage || { get: async () => null, set: async () => {} };
const owner = options.ownerId || token(); let initialized = false; let initializing = null; let destroyed = false; let busy = false; let generation = 0;
let session = null; let activeTaskId = ''; let pageKey = ''; let saveSignature = ''; let pendingQuiz = null; let worker = null;
let lastTime = null; let lastAdvancedAt = now(); let attemptedAt = 0; let playStarted = false;
let interactionAt = -Infinity; let lastAutoPlayAt = -Infinity; let manualVideoPause = false; let watchedDocument = null;
let sharedPaused = false;
const config = { mode: 'all', execution: 'sequential', rate: 1, answerInterval: 4, minSubmitMinutes: 6, associations: {}, stallSeconds: 60, recoverySeconds: 15, maxRefreshes: 3, minRefreshSeconds: 30, videoPaused: false, examPaused: false };
const state = { status: 'idle', mode: 'all', execution: 'sequential', rate: 1, answerInterval: 4, minSubmitMinutes: 6, tasks: [], total: 0, completed: 0, skipped: 0, waiting: 0, failed: 0, etaSeconds: null, video: null, exam: null, logs: [], message: '先识别当前学习计划,再开始本轮任务' };
function snapshot() {
const given = typeof options.getSnapshot === 'function' ? options.getSnapshot() || {} : {};
return Object.assign({ document: root.document, url: root.location ? root.location.href : '', courses: [] }, given);
}
function onInteraction(event) {
if (event.isTrusted && event.target && event.target.closest && event.target.closest('video,.xgplayer')) interactionAt = now();
}
function onVideoPause(event) {
if (event.target && event.target.tagName === 'VIDEO' && interactionAt > lastAutoPlayAt && now() - interactionAt < 2000) manualVideoPause = true;
}
function notify() {
Object.assign(state, { mode: config.mode, execution: config.execution, rate: config.rate, answerInterval: config.answerInterval, minSubmitMinutes: config.minSubmitMinutes, videoPaused: config.videoPaused, examPaused: config.examPaused });
const counted = state.tasks.filter(t => t.status !== 'expanded' && allows(t.kind));
state.total = counted.length; state.completed = counted.filter(t => t.status === 'completed').length;
state.skipped = counted.filter(t => t.status === 'skipped').length; state.waiting = counted.filter(t => t.status === 'waiting').length; state.failed = counted.filter(t => t.status === 'failed').length;
const todo = counted.filter(t => !TERMINAL.includes(t.status));
const estimates = todo.map(t => {
if (t.status === 'waiting' || t.kind === 'course' || t.kind === 'unsupported') return null;
if (t.kind === 'video') {
const v = state.video && state.video.taskId === t.id ? state.video : t;
if (!(v.duration > 0)) return null;
const playback = Math.max(0, v.duration - (v.currentTime || 0)) / (v.actualRate || config.rate);
// 播放时长已知仍不能推定平台学习条件,未知条件须明确显示未知。
if (v.requiredSeconds == null || v.learnedSeconds == null) return null;
return Math.max(playback, Math.max(0, v.requiredSeconds - v.learnedSeconds));
}
const e = state.exam && state.exam.taskId === t.id ? state.exam : null;
if (!e || e.total == null || ['waiting', 'paused', 'unknown'].includes(e.stage || e.status)) return null;
const elapsed = e.elapsedSeconds != null ? e.elapsedSeconds : e.enteredAt != null ? (now() - e.enteredAt) / 1000 : null;
if (elapsed == null && e.waitSeconds == null) return null;
return Math.max(0, (e.total - (e.verified || 0)) * config.answerInterval, e.waitSeconds != null ? e.waitSeconds : config.minSubmitMinutes * 60 - elapsed);
});
state.etaSeconds = estimates.some(e => e == null) ? null : config.execution === 'parallel' ? Math.max(0, ...['video', 'exam'].map(kind => estimates.reduce((n, e, i) => n + (todo[i].kind === kind ? e : 0), 0))) : estimates.reduce((n, e) => n + e, 0);
if (typeof options.onState === 'function') options.onState(clone(state));
}
function log(level, message, task) {
state.logs.push({ time: now(), level: ['info', 'warn', 'error'].includes(level) ? level : 'info', message: String(message), task: task && task.title || '', taskId: task && task.id || '' });
state.logs = state.logs.slice(-500); notify();
}
function taskState(task, status, reason) {
const changed = task.status !== status || task.reason !== (reason || ''); task.status = status; task.reason = reason || '';
if (changed) log(status === 'waiting' || status === 'failed' ? 'warn' : 'info', reason || status, task);
}
function allows(kind) { return kind === 'course' || kind === 'unsupported' || kind === 'video' && config.mode !== 'exam' || kind === 'exam' && (config.mode === 'exam' || config.mode === 'all'); }
function account(snap, page) { return clean(snap.accountId || page.accountId || ''); }
async function persist() {
if (!session) return;
session.config = clone(config); session.tasks = clone(state.tasks); session.logs = clone(state.logs); session.activeTaskId = activeTaskId; session.updatedAt = now();
// 执行器只写自己领取的任务检查点,主协调器合并,避免并行页面覆盖整份清单。
if (worker) {
const own = state.tasks.find(t => t.id === (activeTaskId || worker.taskId));
if (own) await storage.set('task:' + session.id + ':' + own.id, clone(own));
return;
}
const currentControl = await storage.get(CONTROL_KEY);
if (currentControl && currentControl.sessionId !== session.id) return;
const signature = JSON.stringify([session.tasks, session.config, session.activeTaskId, session.status, session.accountId, session.planId, session.bankVersion]);
if (signature !== saveSignature) { await storage.set(SESSION_KEY, clone(session)); saveSignature = signature; }
}
async function release() {
for (const kind of ['video', 'exam']) { const key = 'lease:' + kind; const lease = await storage.get(key); if (lease && lease.owner === owner) await storage.set(key, null); }
}
async function claim(task) {
const kind = task.kind === 'course' ? 'video' : task.kind;
if (kind !== 'video' && kind !== 'exam') return true;
async function acquire() {
const key = 'lease:' + kind; const lease = await storage.get(key);
const shared = await storage.get(CONTROL_KEY);
if (shared && (shared.sessionId !== session.id || shared.status !== 'running')) return false;
if (lease && lease.owner !== owner && lease.sessionId === session.id && lease.expires > now()) return false;
const wanted = { owner, taskId: task.id, sessionId: session.id, expires: now() + LEASE_MS };
await storage.set(key, wanted);
// GM存储无CAS;延后回读并在每次动作前复核。支持Web Locks时同源标签领取是互斥的。
if (!(root.navigator && root.navigator.locks)) await new Promise(resolve => root.setTimeout(resolve, 25));
const saved = await storage.get(key); return Boolean(saved && saved.owner === owner && saved.taskId === task.id);
}
return root.navigator && root.navigator.locks ? root.navigator.locks.request('yian-learning-' + kind, acquire) : acquire();
}
async function heartbeat(task) {
const key = 'lease:' + (task.kind === 'course' ? 'video' : task.kind); const lease = await storage.get(key);
if (!lease || lease.owner !== owner || lease.taskId !== task.id || lease.sessionId !== session.id) return false;
const shared = await storage.get(CONTROL_KEY); if (shared && (shared.sessionId !== session.id || shared.status !== 'running')) return false;
lease.expires = now() + LEASE_MS; await storage.set(key, lease); return true;
}
function mergeTasks(tasks, parent) {
tasks.forEach(task => {
if (parent && task.id !== parent.id && task.kind !== 'course') task.parentCourse = parent.id;
const current = state.tasks.find(t => t.id === activeTaskId);
// 开考前详情路由与试卷路由不同;同一已授权转场不再生成第二场考试。
const alias = task.current && current && current.kind === task.kind &&
(task.kind === 'exam' && (current.enterClickedAt || current.examLinkClickedAt || current.navigationRequestedAt) && task.title === current.title || task.kind === 'video' && task.title === current.title);
const old = state.tasks.find(t => t.id === task.id || task.identity && t.kind === task.kind && t.identity === task.identity) || (alias ? current : null);
if (!old) state.tasks.push(task);
else {
if (alias && task.url) old.url = task.url;
if (task.status === 'completed') { old.status = 'completed'; old.progress = 1; old.reason = '平台确认已完成'; }
}
});
}
function resolveNode(task, snap) {
if (canonical(snap.url) !== canonical(task.pageUrl) || !task.selector) return null;
let node = snap.document.querySelectorAll(task.selector)[task.index];
if (node && task.subSelector) node = node.querySelectorAll(task.subSelector)[task.subIndex];
if (!node || !visible(node)) return null;
const actualId = node.getAttribute('data-task-id') || node.getAttribute('data-courseware-id') || node.getAttribute('data-course-id') || node.getAttribute('data-exam-id') || node.id;
if (task.identity && actualId !== task.identity) return null;
const title = clean((node.querySelector('.activity_name,:scope > p,[data-task-title]') || node).textContent);
if (!task.identity && title !== task.title) return null;
return node;
}
function initialize() {
if (initialized) return Promise.resolve();
if (initializing) return initializing;
initializing = initializeWork().then(() => { initialized = true; }).finally(() => { initializing = null; });
return initializing;
}
async function initializeWork() {
const snap = snapshot(); if (!snap.document) return;
watchedDocument = snap.document; watchedDocument.addEventListener('pointerdown', onInteraction, true); watchedDocument.addEventListener('keydown', onInteraction, true); watchedDocument.addEventListener('pause', onVideoPause, true);
const page = describePage(snap.document, snap.url);
const saved = await storage.get(SESSION_KEY);
const marker = new URL(snap.url || 'http://invalid').hash.match(/(?:^#|&)__yian_worker=([^&]+)/);
const recover = await storage.get('recover:' + canonical(snap.url));
const transition = await storage.get('transition.v1');
if (saved && Array.isArray(saved.tasks)) {
const identityOK = (!saved.accountId || saved.accountId === account(snap, page)) && (!saved.planId || !snap.planId || saved.planId === snap.planId) && (!saved.bankVersion || !snap.bankVersion || saved.bankVersion === snap.bankVersion);
if (identityOK) {
session = saved; Object.assign(config, saved.config || {}); state.tasks = saved.tasks; state.logs = Array.isArray(saved.logs) ? saved.logs.slice(-500) : []; activeTaskId = saved.activeTaskId || '';
if (marker) {
const request = await storage.get('worker:' + marker[1]);
const control = await storage.get(CONTROL_KEY);
if (request && control && request.sessionId === session.id && request.url === canonical(snap.url) && request.expires > now() && ['running', 'paused'].includes(control.status)) {
worker = request; activeTaskId = request.taskId; state.status = control.status; sharedPaused = control.status === 'paused'; request.owner = owner; request.confirmedAt = now(); await storage.set('worker:' + marker[1], request);
}
}
if (!worker) {
const sharedControl = await storage.get(CONTROL_KEY);
const targetOK = transition && transition.sessionId === session.id && transition.expires > now() && transition.accountId === account(snap, page) &&
(transition.targetURL ? transition.targetURL === page.url : transition.sourceURL !== page.url && transition.title === page.title && ['course', 'video', 'exam', 'paper'].includes(page.kind));
const controlled = sharedControl && sharedControl.status === 'running' && (recover && recover.sessionId === session.id && recover.taskId === activeTaskId && recover.expires > now() && recover.accountId === account(snap, page) || targetOK);
if (targetOK) {
activeTaskId = transition.taskId;
if (transition.workerNonce && sharedControl && sharedControl.status === 'running') {
const request = await storage.get('worker:' + transition.workerNonce);
if (request && request.sessionId === session.id && request.taskId === activeTaskId) { worker = request; worker.owner = owner; worker.confirmedAt = now(); await storage.set('worker:' + worker.nonce, worker); }
}
await storage.set('transition.v1', null);
}
if (controlled) { state.status = 'running'; session.status = 'running'; await storage.set('recover:' + canonical(snap.url), null); log('info', '已核对脚本恢复标记,继续同一视频任务'); }
else { state.status = 'paused'; session.status = 'paused'; state.message = '已恢复上次任务清单,人工刷新后默认暂停;点击继续重新核对'; }
}
} else { state.message = '账号、学习计划或题库版本已变化,未恢复旧任务'; log('warn', state.message); }
}
if (worker && session) { const checkpoint = await storage.get('task:' + session.id + ':' + activeTaskId); const task = state.tasks.find(t => t.id === activeTaskId); if (checkpoint && task) Object.assign(task, checkpoint); }
mergeTasks(discoverTasks(snap.document, snap.url)); notify();
}
async function control(status, force, explicitWorkerAction) {
if (!session || worker && !explicitWorkerAction) return;
const current = await storage.get(CONTROL_KEY);
if (!force && current && current.sessionId !== session.id) return;
await storage.set(CONTROL_KEY, { sessionId: session.id, status, config: clone(worker && current ? current.config : config), updatedAt: now(), owner: worker && current ? current.owner : owner });
}
async function configure(patch) {
await initialize(); patch = patch || {}; const wasRunning = state.status === 'running';
if (patch.retryDelaySeconds != null) patch = Object.assign({}, patch, { recoverySeconds: patch.retryDelaySeconds });
if (patch.maxRefresh != null) patch = Object.assign({}, patch, { maxRefreshes: patch.maxRefresh });
const modeChanged = patch.mode && patch.mode !== config.mode || patch.execution && patch.execution !== config.execution;
if (MODES.includes(patch.mode)) config.mode = patch.mode;
if (['sequential', 'parallel'].includes(patch.execution)) config.execution = patch.execution;
for (const key of ['rate', 'answerInterval', 'minSubmitMinutes', 'stallSeconds', 'recoverySeconds', 'maxRefreshes', 'minRefreshSeconds']) {
if (Number.isFinite(Number(patch[key]))) config[key] = Math.max(key === 'rate' ? 0.25 : key === 'answerInterval' || key === 'minSubmitMinutes' ? 0 : 1, Number(patch[key]));
}
config.rate = Math.min(2, config.rate); config.maxRefreshes = Math.min(3, Math.floor(config.maxRefreshes));
if (patch.associations && typeof patch.associations === 'object') config.associations = clone(patch.associations);
if (typeof patch.videoPaused === 'boolean') config.videoPaused = patch.videoPaused;
if (typeof patch.examPaused === 'boolean') config.examPaused = patch.examPaused;
if (patch.clearLogs) state.logs = [];
if (modeChanged && wasRunning) await pause('执行模式已改变,请核对任务后继续');
if (config.videoPaused) { const snap = snapshot(); const v = snap.document && describePage(snap.document, snap.url).video; if (v) v.pause(); }
if (config.examPaused && typeof options.onExamPause === 'function') options.onExamPause('考试任务已局部暂停');
await control(state.status); await persist(); notify(); return getState();
}
async function start() {
await initialize(); if (destroyed) return getState();
const snap = snapshot(); const page = describePage(snap.document, snap.url);
if (!safeURL(snap.url, snap.url)) { state.status = 'paused'; state.message = '当前页面不在已授权站点目录'; notify(); return getState(); }
const previous = session;
config.videoPaused = false; config.examPaused = false;
session = { id: token(), accountId: account(snap, page), planId: snap.planId || page.url, bankVersion: snap.bankVersion || '', status: 'running', createdAt: now(), tasks: [], config: clone(config), logs: clone(state.logs) };
state.tasks = []; activeTaskId = ''; worker = null; saveSignature = ''; mergeTasks(discoverTasks(snap.document, snap.url));
// 全新一轮不沿用旧执行状态,但保留平台本身已完成标记。
if (previous) await release();
state.status = 'running'; sharedPaused = false; state.message = '正在按当前模式核对任务'; generation++; await control('running', true); await persist(); log('info', '开始本轮学习:' + config.mode + ' / ' + config.execution);
return tick();
}
async function pause(reason) {
await initialize(); generation++; state.status = 'paused'; sharedPaused = true; state.message = reason || '已暂停全部任务;网站考试倒计时仍会继续';
if (session) session.status = 'paused'; const snap = snapshot(); const page = snap.document && describePage(snap.document, snap.url); if (page && page.video) page.video.pause();
if (typeof options.onExamPause === 'function') options.onExamPause(state.message);
await control('paused', false, true); await release(); await persist(); log('info', state.message); return getState();
}
async function resume() {
await initialize(); if (!session) return start();
const snap = snapshot(); const page = describePage(snap.document, snap.url);
if (session.accountId && session.accountId !== account(snap, page) || session.bankVersion && snap.bankVersion && session.bankVersion !== snap.bankVersion) {
state.message = '账号或题库版本已变化,请开始新一轮任务'; state.status = 'paused'; notify(); return getState();
}
state.tasks.filter(t => (t.status === 'waiting' || ACTIVE.includes(t.status) || t.status === 'waitingPlatform') && !t.locked).forEach(t => { t.status = 'pending'; t.reason = ''; t.examRequested = false; });
config.videoPaused = false; config.examPaused = false;
generation++; state.status = 'running'; sharedPaused = false; session.status = 'running'; activeTaskId = ''; playStarted = false; attemptedAt = 0; lastTime = null; lastAdvancedAt = now();
await control('running', false, true); await persist(); log('info', '继续任务,重新核验页面实际状态'); return tick();
}
async function stop() {
await pause('已停止本轮;保留网站已发生的学习和作答记录'); state.status = 'stopped'; if (session) session.status = 'stopped'; await control('stopped'); await persist(); notify(); return getState();
}
async function activate(task, snap, page) {
if (task.locked) { taskState(task, 'waiting', '网站尚未开放此任务'); return false; }
if (task.requiresParent && task.parentId) { const parent = state.tasks.find(t => t.id === task.parentId); if (parent && parent.status !== 'completed') return false; }
if (task.kind === 'exam') {
task.preflight = preflightTopic(snap.document, snap, config.associations, task);
if (task.preflight.status !== 'matched') { taskState(task, task.preflight.status === 'missing' ? 'skipped' : 'waiting', task.preflight.reason); return false; }
}
if (!(await claim(task))) { taskState(task, 'waiting', '另一个页面正在处理同类任务,避免重复领取'); return false; }
activeTaskId = task.id; playStarted = false; attemptedAt = 0; lastTime = null; lastAdvancedAt = now(); manualVideoPause = false;
const onPage = canonical(task.url || task.pageUrl) === page.url;
const isCurrentVideo = task.kind === 'video' && page.video && (task.current || resolveNode(task, snap) === page.activeCourseware);
const isCurrentExam = task.kind === 'exam' && ['exam', 'paper'].includes(page.kind) && onPage;
if (isCurrentVideo || isCurrentExam) { taskState(task, 'loading', '已领取当前页面任务'); return true; }
const node = resolveNode(task, snap);
taskState(task, 'loading', '正在通过网页现有入口打开任务');
await persist();
const targetURL = task.url || (canonical(task.pageUrl) !== page.url ? task.pageUrl : '');
task.navigationRequestedAt = now();
await storage.set('transition.v1', { sessionId: session.id, taskId: task.id, workerNonce: worker && worker.nonce || '', sourceURL: page.url, title: task.title, targetURL: canonical(targetURL), accountId: account(snap, page), expires: now() + 60000 });
if (node) { (node.matches('.activity_content') && node.querySelector('.map_activity_content_info_top') || node).click(); return true; }
if (task.url && typeof options.navigate === 'function' && safeURL(task.url, snap.url)) { await options.navigate(task.url); return true; }
if (canonical(task.pageUrl) !== page.url && safeURL(task.pageUrl, snap.url) && typeof options.navigate === 'function') { await options.navigate(task.pageUrl); return true; }
taskState(task, 'waiting', '找不到原任务入口,请打开该课程后继续'); activeTaskId = ''; await release(); return false;
}
async function videoTick(task, snap, page, epoch) {
if (config.videoPaused) { if (page.video) page.video.pause(); taskState(task, 'waiting', '视频任务已局部暂停'); return; }
if (!page.video) {
if (task.selector) { const node = resolveNode(task, snap); if (node && task.status === 'loading' && !task.openedAt) { task.openedAt = now(); node.click(); return; } }
if (!task.openedAt) task.openedAt = now();
if (now() - task.openedAt > 15000) taskState(task, 'waiting', '播放器未加载或自动播放需要用户操作');
return;
}
const video = page.video;
if (page.activeCourseware && !task.current && task.title !== clean((page.activeCourseware.querySelector(':scope > p') || page.activeCourseware).textContent)) {
taskState(task, 'waiting', '当前选中课件与已领取任务不同,请核对后继续'); return;
}
if (page.platformComplete) {
state.video = { taskId: task.id, title: task.title, currentTime: Number.isFinite(video.currentTime) ? video.currentTime : 0,
duration: Number.isFinite(video.duration) ? video.duration : null, actualRate: video.playbackRate,
learnedSeconds: page.learnedSeconds, requiredSeconds: page.requiredSeconds, platformComplete: true,
background: page.hidden, status: 'completed', retryCount: task.retryCount || 0 };
task.progress = 1; taskState(task, 'completed', '平台已确认课件学习完成'); activeTaskId = ''; await release(); return;
}
const popup = page.quiz;
const currentTime = Number.isFinite(video.currentTime) ? video.currentTime : 0; const duration = Number.isFinite(video.duration) && video.duration > 0 ? video.duration : null;
state.video = { taskId: task.id, title: task.title, currentTime, duration, actualRate: video.playbackRate, learnedSeconds: page.learnedSeconds, requiredSeconds: page.requiredSeconds, platformComplete: false, background: page.hidden, status: 'playing', retryCount: task.retryCount || 0 };
Object.assign(task, { currentTime, duration, learnedSeconds: page.learnedSeconds, requiredSeconds: page.requiredSeconds, progress: duration ? Math.min(1, currentTime / duration) : 0 });
if (popup) {
video.pause(); lastAdvancedAt = now(); attemptedAt = 0; playStarted = false;
if (config.mode === 'video') { taskState(task, 'waiting', '仅视频模式遇到强制课中题,请人工完成后继续'); state.video.status = 'waiting'; return; }
taskState(task, 'quiz', '正在核对课中题完整文字'); state.video.status = 'quiz';
if (typeof options.solveQuiz !== 'function') { taskState(task, 'waiting', '缺少此课中题结构适配,请人工处理;不会猜选或刷新'); return; }
if (pendingQuiz) return;
pendingQuiz = popup;
try {
const result = await options.solveQuiz(popup, { task: clone(task), answerInterval: config.answerInterval, document: snap.document });
if (epoch !== generation || state.status !== 'running') return;
if (!result || result.status !== 'completed' || result.verified !== true || quizOf(snap.document)) taskState(task, 'waiting', result && result.reason || '课中题尚未确认提交成功,请人工核对后继续');
else { taskState(task, 'playing', '课中题已核验且弹题已关闭,继续视频'); lastAdvancedAt = now(); }
} catch (error) { if (epoch === generation) taskState(task, 'waiting', '课中题处理失败:' + error.message); }
finally { pendingQuiz = null; }
return;
}
if (manualVideoPause) { taskState(task, 'waiting', '用户已在播放器主动暂停,请点击继续恢复任务'); state.video.status = 'waiting'; return; }
if (video.ended || duration && currentTime >= duration - 0.05) { taskState(task, 'waitingPlatform', '视频已播放到末尾,等待平台确认学习完成'); state.video.status = 'waitingPlatform'; attemptedAt = 0; return; }
if (!Number.isFinite(video.playbackRate) || Math.abs(video.playbackRate - config.rate) > 0.01) { try { video.playbackRate = config.rate; } catch (_) {} }
state.video.actualRate = video.playbackRate;
if (Math.abs(video.playbackRate - config.rate) > 0.01) state.video.rateMessage = '网站未接受所选倍速,当前实际 ' + video.playbackRate + 'X';
if (!playStarted) {
playStarted = true;
lastAutoPlayAt = now();
try { const promise = video.play(); if (promise && promise.catch) await promise; }
catch (_) { taskState(task, 'waiting', '浏览器拒绝自动播放,请手动播放后继续'); state.video.status = 'waiting'; return; }
if (epoch !== generation || state.status !== 'running') { video.pause(); return; }
lastAdvancedAt = now();
}
if (lastTime === null || Math.abs(currentTime - lastTime) > 0.01) { lastTime = currentTime; lastAdvancedAt = now(); attemptedAt = 0; taskState(task, 'playing', page.hidden ? '页面在后台,按真实播放与平台记录跟踪' : '视频播放中'); return; }
if (!attemptedAt && now() - lastAdvancedAt >= config.stallSeconds * 1000) {
attemptedAt = now(); taskState(task, 'retrying', '播放进度长时间未变化,先尝试续播');
try { await video.play(); } catch (_) { taskState(task, 'waiting', '续播被浏览器拒绝,请人工操作'); }
return;
}
if (attemptedAt && now() - attemptedAt >= config.recoverySeconds * 1000) {
if ((task.retryCount || 0) >= config.maxRefreshes) { taskState(task, 'failed', '已达到本课件最多 ' + config.maxRefreshes + ' 次恢复上限,请人工处理'); state.video.status = 'failed'; return; }
if (task.lastRefreshAt && now() - task.lastRefreshAt < config.minRefreshSeconds * 1000) return;
if (!await heartbeat(task) || epoch !== generation || state.status !== 'running') return;
task.retryCount = (task.retryCount || 0) + 1; task.lastRefreshAt = now(); taskState(task, 'retrying', '视频续播失败,保存检查点后刷新当前视频页(' + task.retryCount + '/' + config.maxRefreshes + ')');
await persist();
await storage.set('recover:' + page.url, { sessionId: session.id, taskId: task.id, accountId: account(snap, page), expires: now() + 60000 });
if (typeof options.reload === 'function') await options.reload(); else if (root.location && canonical(root.location.href) === page.url) root.location.reload();
attemptedAt = 0; lastAdvancedAt = now();
}
}
async function examTick(task, snap, page) {
if (config.examPaused) { taskState(task, 'waiting', '考试任务已局部暂停'); return; }
if (!['exam', 'paper'].includes(page.kind)) {
const node = resolveNode(task, snap);
if (node && !task.examLinkClickedAt) {
task.examLinkClickedAt = now(); taskState(task, 'loading', '通过课程目录的原始关联考试入口打开考试'); await persist();
await storage.set('transition.v1', { sessionId: session.id, taskId: task.id, workerNonce: worker && worker.nonce || '', sourceURL: page.url, targetURL: '', title: task.title, accountId: account(snap, page), expires: now() + 60000 });
node.click();
} else if (task.examLinkClickedAt && now() - task.examLinkClickedAt > 15000) taskState(task, 'waiting', '关联考试入口未进入考试,请核对网站限制');
return;
}
task.preflight = preflightTopic(snap.document, snap, config.associations, task);
if (task.preflight.status !== 'matched') { taskState(task, task.preflight.status === 'missing' ? 'skipped' : 'waiting', task.preflight.reason); return; }
if (page.kind === 'exam') {
const blockers = Array.from(snap.document.querySelectorAll('.exam_qrcode_box,[data-auth-required],.ant-modal-wrap')).filter(node => visible(node) && /人脸|身份验证|考前须知|登录/.test(clean(node.textContent)));
if (blockers.length) { taskState(task, 'waiting', '考试需要身份验证或阅读须知,请人工完成后继续'); return; }
if (task.enterClickedAt) { if (now() - task.enterClickedAt > 15000) taskState(task, 'waiting', '开考页面未进入试卷,请人工检查限制或提示'); return; }
const buttons = Array.from(snap.document.querySelectorAll('.examdetail_button')).filter(node => visible(node) && !node.matches('[disabled],[aria-disabled="true"]') && /^(?:开始答题|继续答题|开始考试|继续考试)$/.test(clean(node.textContent)));
if (buttons.length !== 1) { taskState(task, 'waiting', '找不到唯一有效的正常开考入口,请人工进入试卷'); return; }
task.enterClickedAt = now(); taskState(task, 'loading', '主题预检通过,通过网页正常按钮进入考试'); await persist();
await storage.set('transition.v1', { sessionId: session.id, taskId: task.id, workerNonce: worker && worker.nonce || '', sourceURL: page.url, targetURL: '', title: task.title, accountId: account(snap, page), expires: now() + 60000 });
buttons[0].click(); return;
}
if (!task.examRequested || task.examPage !== page.kind) {
task.examPage = page.kind; taskState(task, 'answering', '考试主题已确认,交给答题与交卷控制器');
if (typeof options.onExamStart === 'function') {
const accepted = await options.onExamStart({ task: clone(task), preflight: clone(task.preflight), document: snap.document, page: page.kind, config: clone(config) });
task.examRequested = accepted !== false;
if (accepted === false) { if (!task.readyWaitAt) task.readyWaitAt = now(); if (now() - task.readyWaitAt > 15000) taskState(task, 'waiting', '试卷未完整加载,请人工核对后继续'); }
}
else taskState(task, 'waiting', '考试控制器尚未接入,请人工处理');
}
const exam = typeof options.getExamState === 'function' ? options.getExamState() : null;
if (exam) {
state.exam = Object.assign({ taskId: task.id, title: task.title }, clone(exam));
state.exam.total = exam.total == null && exam.details ? exam.details.length : exam.total;
state.exam.verified = exam.verifiedCount == null ? exam.verified : exam.verifiedCount;
const stage = exam.stage || exam.status;
if (stage === 'completed' || stage === 'submitted' || exam.submitted === true && exam.confirmed === true) { task.progress = 1; taskState(task, 'completed', '网站已确认本场考试提交完成'); activeTaskId = ''; await release(); }
else if (['waiting', 'paused', 'failed', 'unknown', 'awaitingAttempt'].includes(stage)) taskState(task, 'waiting', exam.reason || exam.message || '考试等待人工处理');
else if (stage === 'waitingSubmit') taskState(task, 'waitingSubmit', exam.reason || '已核验,等待最早交卷时间');
}
}
async function parallelTick(snap, page) {
if (config.execution !== 'parallel' || worker || !session) return;
const local = state.tasks.find(t => t.id === activeTaskId); const opposite = local && local.kind === 'exam' ? 'video' : 'exam';
for (const t of state.tasks) {
const checkpoint = await storage.get('task:' + session.id + ':' + t.id);
if (checkpoint) Object.assign(t, checkpoint);
if (t.workerNonce) {
const status = await storage.get('worker:' + t.workerNonce);
if (status && status.confirmedAt && status.owner && (!status.heartbeatAt || now() - status.heartbeatAt < LEASE_MS)) {
if (status.video && (!local || local.kind !== 'video')) state.video = clone(status.video);
if (status.exam && (!local || local.kind !== 'exam')) state.exam = clone(status.exam);
}
}
}
const requested = state.tasks.find(t => t.workerNonce && !TERMINAL.includes(t.status));
if (requested) {
const request = await storage.get('worker:' + requested.workerNonce);
if (request && request.confirmedAt && request.owner && (!request.heartbeatAt || now() - request.heartbeatAt < LEASE_MS)) {
requested.status = request.status || 'loading'; state.message = '并行执行器已确认启动:最多一个视频和一场考试';
if (request.video && (!local || local.kind !== 'video')) state.video = clone(request.video);
if (request.exam && (!local || local.kind !== 'exam')) state.exam = clone(request.exam);
}
else if (!request || request.expires <= now()) { taskState(requested, 'waiting', '另一个页面未确认启动,可能被浏览器拦截;请打开任务后继续'); }
else state.message = '正在等待另一执行页面确认,尚未开始并行';
return;
}
if (!local || !['video', 'exam'].includes(local.kind)) return;
const next = state.tasks.find(t => t.kind === opposite && t.status === 'pending' && allows(t.kind) && !t.locked && (!t.requiresParent || !t.parentId || state.tasks.find(p => p.id === t.parentId && p.status === 'completed')));
if (!next) return;
if (next.kind === 'exam') { next.preflight = preflightTopic(snap.document, snap, config.associations, next); if (next.preflight.status !== 'matched') { taskState(next, next.preflight.status === 'missing' ? 'skipped' : 'waiting', next.preflight.reason); return; } }
// Angular目录没有href时,先打开已经访问过的真实课程URL,再在工作页点击原目录节点。
const targetURL = next.url || next.selector && next.pageUrl || '';
if (!targetURL || !safeURL(targetURL, snap.url)) { taskState(next, 'waiting', '并行任务缺少网页真实链接或可恢复目录,请在另一页面打开对应任务'); return; }
if (typeof options.openWorker !== 'function') { taskState(next, 'waiting', '当前环境不能创建受控执行页面,请人工打开任务'); return; }
const nonce = token(); const url = new URL(targetURL); url.hash = (url.hash ? url.hash.slice(1) + '&' : '') + '__yian_worker=' + nonce;
next.workerNonce = nonce; next.status = 'loading';
await storage.set('worker:' + nonce, { nonce, sessionId: session.id, taskId: next.id, url: canonical(targetURL), expires: now() + 30000 }); await persist();
try { const opened = await options.openWorker(url.href); if (opened === false || opened === null) taskState(next, 'waiting', '并行页面被浏览器拦截,请人工打开任务'); else log('info', '已请求打开执行页面,等待其心跳确认', next); }
catch (_) { taskState(next, 'waiting', '并行页面打开失败,请人工处理'); }
}
async function tick() {
if (destroyed) return getState();
// 课中题异步处理期间仍响应另一标签页的全局暂停,不能等整段答题结束。
if (busy) {
if (await observeControl()) return getState();
const task = state.tasks.find(t => t.id === activeTaskId);
if (state.status === 'running' && task && !await heartbeat(task)) {
// 弹题等待中的旧异步流程必须立即失效,getActive 随状态变更阻止后续提交。
generation++; state.status = 'paused'; sharedPaused = false;
state.message = '当前任务租约已失效或被另一执行器领取,旧答题流程已暂停';
if (session) session.status = 'paused';
const snap = snapshot(); const page = snap.document && describePage(snap.document, snap.url);
if (page && page.video) page.video.pause();
if (state.video) state.video.status = 'waiting';
if (options.onExamPause) options.onExamPause(state.message);
taskState(task, 'waiting', state.message); await release(); notify();
}
return getState();
}
busy = true; const epoch = generation;
try {
await initialize(); const snap = snapshot(); if (!snap.document) return getState();
const page = describePage(snap.document, snap.url);
if (!safeURL(snap.url, snap.url)) { if (state.status === 'running') await pause('离开已授权站点,任务已暂停'); return getState(); }
if (session && (state.status === 'running' || sharedPaused)) {
const controlValue = await storage.get(CONTROL_KEY);
if (await observeControl(controlValue)) return getState();
if (worker && controlValue && controlValue.config) Object.assign(config, controlValue.config);
if (session.accountId && session.accountId !== account(snap, page) || session.bankVersion && snap.bankVersion && session.bankVersion !== snap.bankVersion) { await pause('账号或题库版本改变,已暂停旧任务'); return getState(); }
}
if (pageKey !== page.url) { pageKey = page.url; playStarted = false; lastTime = null; attemptedAt = 0; lastAdvancedAt = now(); }
let task = state.tasks.find(t => t.id === activeTaskId);
if (task && task.kind === 'course' && page.kind === 'course' || task && task.kind === 'course' && page.kind === 'video') {
if (task.title !== page.title && canonical(task.url || task.pageUrl) !== page.url) taskState(task, 'waiting', '当前课程与已领取任务不同,请核对后继续');
else { mergeTasks(discoverTasks(snap.document, snap.url), task); taskState(task, 'expanded', '已读取课程目录;课件须逐一等待平台确认完成'); activeTaskId = ''; await release(); task = null; }
} else mergeTasks(discoverTasks(snap.document, snap.url));
if (state.status !== 'running') { notify(); return getState(); }
if (task && TERMINAL.includes(task.status)) { activeTaskId = ''; await release(); task = null; }
if (!task) {
const candidates = state.tasks.filter(t => t.status === 'pending' && allows(t.kind) && (!worker || t.id === worker.taskId));
// 当前页面的实际课件优先;顺序执行先完成可用视频,再处理关联考试。
function rank(t) { return (canonical(t.pageUrl) === page.url ? 0 : 10) + (t.kind === 'video' ? 0 : t.kind === 'exam' ? 1 : 2); }
candidates.sort((a, b) => rank(a) - rank(b));
for (const next of candidates) { if (await activate(next, snap, page)) { task = next; break; } }
}
if (task && epoch === generation) {
const held = await heartbeat(task) || await claim(task);
if (!held) state.message = '等待同类任务租约释放,暂不操作当前页面';
else if (task.status !== 'waiting' && task.kind === 'video') await videoTick(task, snap, page, epoch);
else if (task.status !== 'waiting' && task.kind === 'exam') await examTick(task, snap, page);
}
if (epoch === generation) await parallelTick(snap, page);
if (worker && session) { const request = await storage.get('worker:' + worker.nonce); if (request) { const own = task || state.tasks.find(t => t.id === worker.taskId); request.heartbeatAt = now(); request.status = own && own.status || 'idle'; request.video = state.video; request.exam = state.exam; await storage.set('worker:' + worker.nonce, request); } }
const remaining = state.tasks.filter(t => allows(t.kind) && !TERMINAL.includes(t.status));
if (!task && !remaining.length) { state.status = state.tasks.length ? 'finished' : 'waiting'; state.message = state.tasks.length ? '本轮调度结束;请检查完成、跳过及失败清单' : '当前页面没有可识别的任务,请打开学习地图、课程或试卷'; if (session) session.status = state.status; await control(state.status); }
else if (!task && remaining.every(t => t.status === 'waiting')) { state.message = '剩余任务等待人工处理;确认后点击继续'; }
await persist(); notify(); return getState();
} catch (error) { state.message = '学习调度异常:' + error.message; state.status = 'paused'; if (session) session.status = 'paused'; await control('paused'); await release(); log('error', state.message); return getState(); }
finally { busy = false; }
}
async function observeControl(value) {
if (!session || state.status !== 'running' && !sharedPaused) return false;
const shared = value === undefined ? await storage.get(CONTROL_KEY) : value;
if (sharedPaused && shared && shared.sessionId === session.id && shared.status === 'running') {
sharedPaused = false; generation++; state.status = 'running'; session.status = 'running'; state.message = '已收到继续全部任务,重新核验当前页面'; playStarted = false; manualVideoPause = false; attemptedAt = 0; lastTime = null; lastAdvancedAt = now();
const active = state.tasks.find(t => t.id === activeTaskId); if (active) { active.examRequested = false; if (active.status === 'waiting') active.status = 'pending'; }
notify(); return false;
}
if (!shared || shared.sessionId === session.id && shared.status === 'running') return false;
if (state.status !== 'running') return false;
generation++; state.status = shared.status === 'stopped' && shared.sessionId === session.id ? 'stopped' : 'paused'; sharedPaused = shared.sessionId === session.id && shared.status === 'paused';
state.message = shared.sessionId !== session.id ? '另一页面已开始新一轮任务,旧执行器已停止' : '全局任务已暂停;网站考试倒计时仍会继续';
const snap = snapshot(); const page = snap.document && describePage(snap.document, snap.url); if (page && page.video) page.video.pause();
if (options.onExamPause) options.onExamPause(state.message); await release(); notify(); return true;
}
function getState() { return clone(state); }
function clearLogs() { state.logs = []; notify(); persist().catch(() => {}); }
function destroy() {
destroyed = true; generation++;
if (watchedDocument) { watchedDocument.removeEventListener('pointerdown', onInteraction, true); watchedDocument.removeEventListener('keydown', onInteraction, true); watchedDocument.removeEventListener('pause', onVideoPause, true); }
if (typeof options.onExamPause === 'function') options.onExamPause('学习控制器已退出'); release().catch(() => {});
}
return { configure, start, pause, resume, stop, tick, getState, destroy, log, clearLogs };
}
return { create, describePage, discoverTasks, preflightTopic };
});
/* Courseware quiz DOM adapter. Templates verified in saved 5-es2015 bundle:
* app-question-modal -> CDK .question-modal-pc, .question_box/.answer_box.
* doCheck changes checked class; 查看答案 grades; 下一题/关闭 emits continuePlay.
* The saved site's scripts are never evaluated by this module. */
(function (root) {
'use strict';
const jobs = new WeakMap();
const CHECKED = 'examtest_content_list_des_checked';
const OWNER = '.question-modal-pc,.question-modal-mobile,app-question-modal,[data-learning-quiz]';
const clean = value => String(value == null ? '' : value).replace(/[\u200b-\u200d\u2060\uFEFF]/g, '').replace(/\s+/g, ' ').trim();
const same = (left, right) => JSON.stringify(left.map(clean).sort()) === JSON.stringify(right.map(clean).sort());
function visible(node) {
if (!node || !node.isConnected || node.closest('[hidden],[aria-hidden="true"],.hidden,[inert]')) return false;
const view = node.ownerDocument.defaultView;
for (let parent = node; parent && parent.nodeType === 1; parent = parent.parentElement) {
const style = view.getComputedStyle(parent);
if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse' || style.opacity === '0') return false;
}
return node.getClientRects().length > 0;
}
function known(node) { return Boolean(node.closest(OWNER)); }
function detect(doc) {
const question = Array.from(doc.querySelectorAll('.question_box')).filter(node => known(node) && visible(node));
const answer = Array.from(doc.querySelectorAll('.answer_box')).filter(node => known(node) && visible(node));
if (question.length > 1 || answer.length > 1 || question.length && answer.length) return { ambiguous: true };
if (answer.length) return { kind: 'portalAnswer', container: answer[0] };
if (question.length) return { kind: 'portal', container: question[0] };
const legacy = Array.from(doc.querySelectorAll('.pc_examtest_content')).filter(node => known(node) && visible(node));
if (legacy.length > 1) return { ambiguous: true };
if (legacy.length) return { kind: 'legacy', container: legacy[0] };
return null;
}
function read(view, context) {
const box = view.container;
if (box.querySelector('img,svg,canvas,video,audio,iframe,math')) return { error: '课中题含图片或非文字内容,需要人工核对' };
let question, nodes;
if (view.kind === 'portal') {
const stems = Array.from(box.querySelectorAll('.examtest_content_question > p')).filter(visible);
if (stems.length !== 1) return { error: '课中题题干无法唯一识别' };
const stem = clean(stems[0].textContent);
const type = stem.match(/^[\[【](单选|多选|判断)(?:题)?[\]】]\s*/);
if (!type) return { error: '课中题缺少明确支持的题型' };
nodes = Array.from(box.querySelectorAll('.examtest_content_list_des')).filter(visible);
const options = nodes.map(node => {
const labels = node.querySelectorAll('.examtest_content_list_des_optionsort');
if (labels.length !== 1) return null;
const label = clean(labels[0].textContent).toUpperCase().replace(/[..、::))]$/, '');
const copy = node.cloneNode(true); copy.querySelector('.examtest_content_list_des_optionsort').remove();
return { label, text: clean(copy.textContent) };
});
if (options.some(option => !option || !/^[A-H]$/.test(option.label) || !option.text)) return { error: '课中题选项文字或字母无法完整识别' };
question = { type: type[1], question: stem.slice(type[0].length), options, number: 1 };
} else {
if (!root.QuestionBankCore) return { error: '课中题提取组件未加载' };
const wrapper = box.ownerDocument.createElement('div'); wrapper.append(box.cloneNode(true));
const bank = root.QuestionBankCore.extract(wrapper, context.sourceUrl);
if (bank.questions.length !== 1) return { error: '课中题结构尚未完整加载' };
question = bank.questions[0]; nodes = Array.from(box.querySelectorAll(root.QuestionBankCore.selectors.option));
}
if (!question.question || nodes.length !== question.options.length || nodes.length < 2 || nodes.length > 8) return { error: '课中题题干及选项尚未完整加载' };
return { question, nodes, signature: JSON.stringify([question.type, question.question, question.options.map(option => [option.label, option.text])]) };
}
function buttons(container, expression) {
return Array.from(container.querySelectorAll('.modal-footer button,.pc_examtest_content_question_showanswer,button[data-quiz-submit]'))
.filter(node => visible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true' && expression.test(clean(node.textContent)));
}
async function run(options) {
const doc = options.document || root.document, matcher = options.matcher || root.YianAnswerMatcher;
const context = options.context || {};
if (!doc || !matcher || typeof matcher.match !== 'function') return { status: 'waiting', verified: false, reason: '课中题匹配组件未就绪' };
let cancelled = false, verifiedCount = 0;
const timeout = Math.max(100, Math.min(15000, Number(options.timeoutMs) || 6000));
const configured = Number(context.answerInterval);
const interval = Math.max(0, Math.min(120, Number.isFinite(configured) ? configured : 4)) * 1000;
const result = (status, reason, extra) => Object.assign({ status, verified: status === 'complete', verifiedCount, reason }, extra || {});
const active = () => {
if (cancelled || options.signal?.aborted) return false;
try { return typeof options.getActive !== 'function' || options.getActive() === true; } catch (_) { return false; }
};
const manual = event => {
if (!event.isTrusted) return;
const node = event.target?.nodeType === 1 ? event.target : event.target?.parentElement;
if (node && known(node) && node.closest('.question_box,.answer_box,.pc_examtest_content')) cancelled = true;
};
doc.addEventListener('click', manual, true);
const pause = milliseconds => new Promise(resolve => {
const started = Date.now();
const step = () => {
if (!active()) return resolve(false);
const remaining = milliseconds - (Date.now() - started);
if (remaining <= 0) return resolve(true);
root.setTimeout(step, Math.min(100, remaining));
};
step();
});
const waitFor = async predicate => {
for (let waited = 0; waited <= timeout; waited += 100) {
if (!active()) return { cancelled: true };
const value = predicate(); if (value) return value;
if (waited < timeout && !await pause(100)) return { cancelled: true };
}
return null;
};
const click = node => { if (!active() || !visible(node) || node.disabled || node.getAttribute('aria-disabled') === 'true') return false; node.click(); return true; };
try {
let view = detect(doc);
if (!view || view.ambiguous || view.kind === 'portalAnswer') return result('waiting', '未发现唯一且可完整核验的课中题,等待人工检查');
for (let questionIndex = 0; questionIndex < 100; questionIndex++) {
if (!active()) return result('waiting', '课中答题已取消,等待重新启动');
const original = read(view, context);
if (original.error) return result('waiting', original.error);
const matched = await matcher.match(original.question, context, options.snapshot);
if (!active()) return result('waiting', '课中答题已取消,等待重新启动');
if (!matched || matched.status !== 'matched') return result(['missing', 'conflict'].includes(matched?.status) ? matched.status : 'unsupported', matched?.reason || '统一题库没有唯一准确答案');
if (!Array.isArray(matched.correctTexts) || !matched.correctTexts.length || !same(matched.correctTexts, original.question.options.filter(option => matched.correctLabels?.includes(option.label)).map(option => option.text))) return result('waiting', '正确答案全文不能对应当前选项,已停止');
const current = () => {
const seen = detect(doc);
if (!seen || seen.ambiguous || seen.container !== view.container || seen.kind !== view.kind) return null;
const latest = read(seen, context); return !latest.error && latest.signature === original.signature ? latest : null;
};
const wanted = original.question.options.map(option => matched.correctTexts.some(text => clean(text) === clean(option.text)));
const plan = [];
original.nodes.forEach((node, i) => { if (!wanted[i] && node.classList.contains(CHECKED)) plan.push({ i, checked: false }); });
original.nodes.forEach((node, i) => { if (wanted[i] && !node.classList.contains(CHECKED)) plan.push({ i, checked: true }); });
if (original.question.type !== '多选') {
if (wanted.filter(Boolean).length !== 1) return result('waiting', '单选或判断题正确答案数量不明');
plan.splice(0, plan.length, ...(original.nodes.every((node, i) => node.classList.contains(CHECKED) === wanted[i]) ? [] : [{ i: wanted.indexOf(true), checked: true }]));
}
for (const step of plan) {
const latest = current();
if (!latest || !click(latest.nodes[step.i])) return result('waiting', '题目或任务状态已变化,停止课中答题');
const changed = await waitFor(() => { const next = current(); return next && next.nodes[step.i].classList.contains(CHECKED) === step.checked ? next : null; });
if (!changed || changed.cancelled) return result('waiting', '网站未确认选中状态或任务已取消');
if (!await pause(150)) return result('waiting', '课中答题已取消');
}
const evidence = current();
if (!evidence || !same(evidence.question.options.filter((_, i) => evidence.nodes[i].classList.contains(CHECKED)).map(option => option.text), matched.correctTexts)) return result('waiting', '课中题实际选中集合与正确答案全文不一致');
const submits = buttons(view.container, /^(查看答案|提交答案|确认答案)$/);
if (submits.length !== 1 || !current() || !click(submits[0])) return result('waiting', '课中题提交入口无法唯一确认');
// The real template replaces the question with an answer overlay immediately;
// absence of the question alone does not imply that the grade request succeeded.
const graded = await waitFor(() => {
const next = detect(doc);
if (next?.ambiguous) return { ambiguous: true };
if (next?.kind === 'portalAnswer') {
const grade = clean(next.container.querySelector('.model_dec')?.textContent);
if (/回答错误/.test(grade)) return { wrong: true };
if (/回答正确/.test(grade)) return next;
}
if (view.kind === 'legacy' && !next && !view.container.isConnected) return { closed: true };
return null;
});
if (!graded || graded.cancelled || graded.ambiguous || graded.wrong) return result('waiting', graded?.wrong ? '网站反馈课中题回答错误,等待人工核对' : '网站尚未确认课中题结果,等待人工核对');
verifiedCount++;
if (graded.closed) return result('complete', '答案全文核验通过,正常提交后弹题已关闭');
const advances = buttons(graded.container, /^(下一题|关闭)$/);
const isLast = advances.length === 1 && clean(advances[0].textContent) === '关闭';
if (advances.length !== 1 || !click(advances[0])) return result('waiting', '课中题已判定正确,但下一题或关闭按钮无法唯一确认');
const next = await waitFor(() => {
const seen = detect(doc);
// CDK detach/attach can briefly render neither overlay between questions.
// Only the actual final 关闭 control authorizes completion by disappearance.
if (!seen) return isLast ? { closed: true } : null;
if (seen.ambiguous) return { ambiguous: true };
if (seen.kind !== 'portalAnswer' && seen.container !== graded.container) return seen;
return null;
});
if (!next || next.cancelled || next.ambiguous) return result('waiting', '课中题已判定正确,等待网页关闭弹题或切到下一题');
if (next.closed) return result('complete', '课中题均核验正确,网站已关闭弹题');
if (!await pause(interval)) return result('waiting', '课中答题已取消');
view = next;
}
return result('waiting', '本组课中题数量超过适配上限,请人工核对');
} catch (error) { return result('waiting', '课中题处理失败:' + String(error.message || error)); }
finally { doc.removeEventListener('click', manual, true); }
}
function solve(options) {
options = options || {}; const doc = options.document || root.document;
if (!doc) return Promise.resolve({ status: 'waiting', verified: false, reason: '当前页面未就绪' });
if (jobs.has(doc)) return jobs.get(doc);
const job = run(options).finally(() => { if (jobs.get(doc) === job) jobs.delete(doc); });
jobs.set(doc, job); return job;
}
root.YianQuizAdapter = { solve };
if (typeof module !== 'undefined' && module.exports) module.exports = root.YianQuizAdapter;
})(typeof window !== 'undefined' ? window : globalThis);
/* Exam evidence, original entry time and bounded real submit interaction. */
(function (root) {
'use strict';
const clean = value => String(value == null ? '' : value).replace(/[\u200b-\u200d\u2060\uFEFF]/g, '').replace(/\s+/g, ' ').trim();
const same = (a, b) => JSON.stringify(a.map(clean).sort()) === JSON.stringify(b.map(clean).sort());
function visible(node) {
if (!node || !node.isConnected || node.closest('[hidden],[aria-hidden="true"],.hidden,[inert]')) return false;
const style = root.getComputedStyle(node);
return node.getClientRects().length > 0 && style.display !== 'none' && style.visibility !== 'hidden';
}
function remainingSeconds(doc) {
const text = clean(doc.querySelector('.pc_examtest_countdown .time')?.textContent);
const parts = text.match(/^(\d{1,3}):(\d{2}):(\d{2})$/);
if (!parts || Number(parts[2]) > 59 || Number(parts[3]) > 59) return null;
return Number(parts[1]) * 3600 + Number(parts[2]) * 60 + Number(parts[3]);
}
function paperKey(url, container) {
const parsed = new URL(url);
const route = parsed.hash.startsWith('#/') ? new URL(parsed.hash.slice(1), parsed.origin) : parsed;
const identities = new Map();
for (const params of [parsed.searchParams, route.searchParams]) for (const [name, value] of params) {
const key = name.toLowerCase().replace(/_/g, '');
if (/^(?:attemptid|examattemptid|answerrecordid|examrecordid|paperrecordid|recordid|examid|paperid|examinationid)$/.test(key) && value) identities.set(key, value);
}
if (container) for (const attribute of ['data-attempt-id', 'data-exam-attempt-id', 'data-answer-record-id', 'data-exam-record-id']) {
const node = container.matches?.('[' + attribute + ']') ? container : container.querySelector?.('[' + attribute + ']');
if (node && clean(node.getAttribute(attribute))) identities.set(attribute, clean(node.getAttribute(attribute)));
}
return route.origin + route.pathname + '|test' + (identities.size ? '|' + JSON.stringify(Array.from(identities).sort()) : '');
}
function readPaper(doc, context, snapshot, topic) {
const core = root.QuestionBankCore, matcher = root.YianAnswerMatcher;
const bank = core.extract(doc, context.sourceUrl);
const boxes = Array.from(doc.querySelectorAll(core.selectors.question));
const details = [];
let totalScore = 0, predictedScore = 0, matchedCount = 0, verifiedCount = 0, validPoints = true;
bank.questions.forEach((question, index) => {
const box = boxes[index];
const pointNodes = box ? Array.from(box.querySelectorAll('.pc_examtest_content_question_value')) : [];
const marks = pointNodes.length === 1 ? clean(pointNodes[0].textContent).match(/^[((]?\s*(\d+(?:\.\d+)?)\s*分\s*[))]?$/) : null;
const points = marks ? Number(marks[1]) : null;
if (!(points > 0 && points <= 100)) validPoints = false;
else totalScore += points;
const match = matcher.match(question, Object.assign({}, context, { courses: topic?.courses || [] }), snapshot);
const nodes = box ? Array.from(box.querySelectorAll(core.selectors.option)) : [];
const currentTexts = question.options.filter((_, i) => nodes[i]?.classList.contains('examtest_content_list_des_checked')).map(option => option.text);
const verified = match.status === 'matched' && same(currentTexts, match.correctTexts);
if (match.status === 'matched') matchedCount++;
if (verified) { verifiedCount++; if (points > 0) predictedScore += points; }
details.push({ number: question.number, question: question.question, points, verified,
status: match.status, reason: !marks ? '分值不明' : verified ? '当前答案全文核验通过' : match.status === 'matched' ? '选中内容尚未核验一致' : match.reason,
correctTexts: match.correctTexts || [], currentTexts });
});
const complete = bank.expectedTotal > 0 && bank.expectedTotal === bank.total && boxes.length === bank.total;
// The confirmed threshold is 80 raw points on the supplied 100-point paper.
const scoreValid = complete && validPoints && Math.abs(totalScore - 100) < 0.000001;
return { title: bank.title, total: bank.total, complete, totalScore, predictedScore, matchedCount, verifiedCount,
missing: details.filter(item => item.status === 'missing').length,
conflict: details.filter(item => item.status === 'conflict').length, scoreValid, details,
signature: JSON.stringify([bank.title, bank.expectedTotal, bank.questions.map((q, index) => [q.type, q.question, q.options.map(o => [o.label, o.text]), details[index].points])]),
reason: !complete ? '等待完整试卷及答题卡' : !scoreValid ? '题目分值或 100 分分制未完整确认,等待人工检查' : '' };
}
function create(options) {
options = options || {};
const doc = options.document || root.document;
const now = options.now || Date.now;
const storage = options.storage || { get: async (_key, fallback) => fallback, set: async () => {} };
let enabled = false, destroyed = false, busy = false, entry = null, currentKey = '', stage = 'idle';
let error = '', last = {}, scope = '', ownSubmit = false, generation = 0, observing = null, observationVersion = 0;
const emit = patch => {
last = Object.assign({}, last, patch, { enabled, stage });
if (options.onState) options.onState(Object.assign({}, last));
return last;
};
const log = message => options.onLog && options.onLog('info', message);
const persist = () => entry ? storage.set('exam:' + currentKey, Object.assign({}, entry)) : Promise.resolve();
function context() { return options.getContext(); }
function keyFor(ctx) { return paperKey(ctx.sourceUrl || root.location.href, ctx.root); }
function sameContext(left, right) {
return left && right && left.root === right.root && left.mode === right.mode && left.revision === right.revision && keyFor(left) === keyFor(right);
}
function timerEvidence(ctx) {
const stamp = now(), remain = remainingSeconds(doc);
const parsed = new URL(ctx.sourceUrl || root.location.href);
const route = parsed.hash.startsWith('#/') ? new URL(parsed.hash.slice(1), parsed.origin) : parsed;
const minutes = Number(route.searchParams.get('totaltime'));
const duration = minutes > 0 && minutes <= 1440 ? minutes * 60 : null;
const strong = Boolean(duration && remain !== null && remain <= duration);
return { stamp, remain, strong, startedAt: strong ? stamp - (duration - remain) * 1000 : stamp };
}
function timerReset(saved, timer) {
if (!Number.isFinite(saved.lastRemaining) || !Number.isFinite(saved.lastObservedAt) || timer.remain === null) return false;
const elapsed = Math.max(0, (timer.stamp - saved.lastObservedAt) / 1000);
return timer.remain - Math.max(0, saved.lastRemaining - elapsed) > 5;
}
function identifiedAttempt(ctx) {
return /"(?:attemptid|examattemptid|answerrecordid|examrecordid|paperrecordid|recordid|data-attempt-id|data-exam-attempt-id|data-answer-record-id|data-exam-record-id)"/.test(keyFor(ctx));
}
function settings() { return options.getSettings ? options.getSettings() : {}; }
function resultVisible() {
return Array.from(doc.querySelectorAll('.exam_submited_box,.exam-result,app-exam-result,[data-exam-result]'))
.some(node => visible(node) && /提交成功|交卷成功|考试结束|得分|成绩/.test(node.textContent));
}
function observe() {
const ctx = context();
if (!ctx || ctx.mode !== 'test' || !ctx.root || destroyed) return Promise.resolve(null);
const key = keyFor(ctx);
if (observing && observing.key === key) return observing.promise;
if (key === currentKey && entry) {
const timer = timerEvidence(ctx);
const resetTimer = timerReset(entry, timer);
const changedPaper = entry.paperSignature && ctx.ready && readPaper(doc, ctx, options.getSnapshot(), options.getTopic()).signature !== entry.paperSignature;
const pending = ['submitting', 'confirming', 'unknown'].includes(entry.stage);
if ((resetTimer || changedPaper) && !pending) {
enabled = false; stage = 'awaitingAttempt'; scope = ''; error = '检测到新试卷或重置的倒计时,请明确启动新场次'; last = { submitted: false };
}
// A stored result belongs to the earlier attempt until the current website
// actually shows that result. Explicit enable may start a fresh visible paper.
if (stage === 'submitted' && !resultVisible() && ctx.exam && !ctx.submitted) {
enabled = false; stage = 'awaitingAttempt'; scope = ''; error = '检测到可作答试卷,请明确启动新场次';
last = { submitted: false };
}
return Promise.resolve(entry);
}
enabled = false; stage = 'idle'; scope = ''; error = ''; currentKey = key; entry = null; last = { submitted: false };
const version = ++observationVersion;
const promise = (async () => {
const timer = timerEvidence(ctx);
const saved = await storage.get('exam:' + key, null);
if (destroyed || version !== observationVersion || key !== currentKey || !sameContext(ctx, context())) return null;
entry = saved && Number.isFinite(saved.startedAt) ? Object.assign({}, saved) : { startedAt: timer.startedAt, initialRemaining: timer.remain, stage: 'idle' };
const pending = ['submitting', 'confirming', 'unknown'].includes(entry.stage);
const expiredCache = Number.isFinite(entry.lastObservedAt) && Number.isFinite(entry.lastRemaining) && timer.remain > 0 && timer.stamp - entry.lastObservedAt > (entry.lastRemaining + 30) * 1000;
const resetTimer = timerReset(entry, timer);
const changedPaper = entry.paperSignature && ctx.ready && readPaper(doc, ctx, options.getSnapshot(), options.getTopic()).signature !== entry.paperSignature;
const unconfirmedClock = saved && timer.remain === null && !identifiedAttempt(ctx);
// Current explicit duration/countdown evidence wins over an older attempt's
// timestamp; taking Math.min here would make a retake immediately eligible.
if (timer.strong && !pending) entry.startedAt = timer.startedAt;
else entry.startedAt = Math.min(timer.stamp, entry.startedAt);
if (pending) { stage = 'unknown'; error = '上次交卷结果尚未确认,请检查网站结果后人工处理'; }
else if (entry.stage === 'submitted' || expiredCache || resetTimer || changedPaper || unconfirmedClock) { stage = 'awaitingAttempt'; error = '旧场次或计时连续性未确认,请明确启动新场次,重新计算最早交卷时间'; }
entry.lastObservedAt = timer.stamp; entry.lastRemaining = timer.remain;
// Preserve a submitted/pending record until explicit new-attempt authorization.
if (stage !== 'awaitingAttempt') await persist();
return !destroyed && version === observationVersion && key === currentKey && sameContext(ctx, context()) ? entry : null;
})().finally(() => { if (observing?.promise === promise) observing = null; });
observing = { key, promise }; return promise;
}
async function enable() {
const token = ++generation, requested = context();
enabled = false;
await observe();
if (token !== generation || !sameContext(requested, context()) || !entry || ['submitting', 'confirming', 'unknown'].includes(stage) || destroyed || resultVisible()) return false;
const ctx = context(), topic = options.getTopic();
if (!topic || topic.status !== 'matched') { error = topic?.reason || '请先确认考试主题与题库关联'; emit({ message: error }); return false; }
const evidence = readPaper(doc, ctx, options.getSnapshot(), topic);
if (!evidence.complete || !evidence.scoreValid || !ctx.ready || !ctx.exam || ctx.submitted) { emit({ message: evidence.reason || '等待整卷加载完成后启动考试任务' }); return false; }
if (stage === 'awaitingAttempt') {
const timer = timerEvidence(ctx);
entry = { startedAt: timer.startedAt, initialRemaining: timer.remain, lastRemaining: timer.remain, lastObservedAt: timer.stamp, stage: 'idle' };
}
scope = JSON.stringify([currentKey, ctx.revision, evidence.signature, topic.courses]);
enabled = true; stage = 'answering'; error = ''; entry.stage = stage; entry.paperSignature = evidence.signature; await persist();
if (token !== generation || destroyed || !enabled || !sameContext(ctx, context()) || !context().ready || options.getTopic()?.status !== 'matched' || scope !== scopeFor(context())) {
if (token === generation) disable('启动过程中页面、题库或任务状态已变化,请重新启动'); return false;
}
log('当前考试已授权:全文核验后严格大于 80 分且达到最早交卷时间才交卷');
return true;
}
function disable(reason) {
generation++;
enabled = false;
if (!['submitting', 'confirming', 'submitted', 'unknown'].includes(stage)) stage = 'paused';
error = reason || '考试自动交卷已暂停';
emit({ message: error });
}
function scopeFor(ctx) {
const topic = options.getTopic(), evidence = readPaper(doc, ctx, options.getSnapshot(), topic);
return JSON.stringify([keyFor(ctx), ctx.revision, evidence.signature, topic?.courses]);
}
function canClick(token, ctx, confirmation) {
if (token !== generation || destroyed || !enabled || !sameContext(ctx, context())) return false;
const current = context(), topic = options.getTopic(), evidence = readPaper(doc, current, options.getSnapshot(), topic);
const configured = Number(settings().minSubmitMinutes), minimum = Number.isFinite(configured) && configured > 0 ? configured * 60 : 360;
const pendingTooLong = confirmation && now() - entry.submitAt > 12000;
return current.mode === 'test' && current.root?.isConnected && current.ready && topic?.status === 'matched'
&& scope === JSON.stringify([keyFor(current), current.revision, evidence.signature, topic.courses])
&& evidence.scoreValid && evidence.matchedCount === evidence.verifiedCount && evidence.predictedScore > 80
&& options.getRunnerState() === 'complete' && (now() - entry.startedAt) / 1000 >= minimum
&& remainingSeconds(doc) !== 0 && !pendingTooLong && (confirmation || current.exam && !current.submitted);
}
function confirmationButton() {
const dialogs = Array.from(doc.querySelectorAll('.dialog_box,[role="dialog"],.modal')).filter(visible);
const containers = dialogs.filter(node => !dialogs.some(other => other !== node && other.contains(node)));
if (!containers.length) return { pending: true };
const matches = containers.filter(node => /(?:确认|确定|是否).*交卷|交卷.*(?:确认|确定)|未答.*(?:提交|交卷)|(?:提交|交卷).*未答/.test(clean(node.textContent)));
if (matches.length !== 1 || /验证|人脸|异常|失败|重新考试/.test(matches[0].textContent)) return { reason: '交卷对话框无法唯一确认,请人工检查' };
const buttons = Array.from(matches[0].querySelectorAll('button,[role="button"],.btn,.submited_btn'))
.filter(node => visible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true' && /^(确定|确认|确认交卷|确定交卷|提交|交卷|继续交卷|仍要交卷)$/.test(clean(node.textContent)));
return buttons.length === 1 ? { button: buttons[0], container: matches[0] } : { reason: '交卷确认按钮无法唯一识别,请人工检查' };
}
async function tick() {
if (destroyed || busy) return last;
busy = true;
const token = generation;
try {
const ctx = context();
if (!ctx || ctx.mode !== 'test' || !ctx.root) { if (enabled) disable('考试页面已切换'); return emit({}); }
await observe();
if (token !== generation || destroyed || !sameContext(ctx, context()) || !entry) return last;
if (resultVisible()) {
stage = 'submitted'; entry.stage = stage; enabled = false; await persist();
if (token !== generation || destroyed || !sameContext(ctx, context())) return last;
return emit({ message: '网站已显示考试结果', submitted: true });
}
const topic = options.getTopic(), evidence = readPaper(doc, ctx, options.getSnapshot(), topic);
const elapsedSeconds = Math.max(0, (now() - entry.startedAt) / 1000);
const configured = Number(settings().minSubmitMinutes);
const minimum = Number.isFinite(configured) && configured > 0 ? configured * 60 : 360;
const waitSeconds = Math.max(0, Math.ceil(minimum - elapsedSeconds));
const remaining = remainingSeconds(doc);
entry.lastObservedAt = now(); entry.lastRemaining = remaining;
emit(Object.assign({}, evidence, { elapsedSeconds: Math.floor(elapsedSeconds), waitSeconds, remainingSeconds: remaining,
predictedScore: Math.round(evidence.predictedScore * 100) / 100, submitted: false, message: error || evidence.reason }));
if (!enabled) return last;
if (scope !== JSON.stringify([currentKey, ctx.revision, evidence.signature, topic?.courses]) || topic?.status !== 'matched') {
disable('试卷、题库或主题关联已变更,请重新启动核验'); return last;
}
if (remaining === 0 || ctx.submitted && !['submitting', 'confirming'].includes(stage)) { disable('网站考试已经结束或提交'); return last; }
if (['submitting', 'confirming'].includes(stage) && now() - entry.submitAt > 12000) {
stage = 'unknown'; entry.stage = stage; enabled = false; await persist();
if (token !== generation || destroyed || !sameContext(ctx, context())) return last;
return emit({ message: '提交结果未确认,已停止重复提交,请检查网站状态' });
}
const runner = options.getRunnerState();
const allVerified = evidence.matchedCount === evidence.verifiedCount;
if (!evidence.scoreValid || !allVerified || runner !== 'complete') {
if (stage === 'confirming' || stage === 'submitting') {
enabled = false; stage = 'unknown'; entry.stage = stage; await persist();
if (token !== generation || destroyed || !sameContext(ctx, context())) return last;
return emit({ message: '交卷过程中核验条件变化,等待人工检查' });
}
stage = 'answering'; return emit({ message: evidence.reason || '等待所有可匹配题处理并回读核验' });
}
if (!(evidence.predictedScore > 80)) { stage = 'waiting'; return emit({ message: '预计得分小于或等于 80 分,等待人工操作' }); }
if (remaining !== null && remaining <= waitSeconds) { stage = 'waiting'; return emit({ message: '网站截止时间早于最早交卷时间,等待人工操作' }); }
if (waitSeconds) { stage = 'waitingSubmit'; return emit({ message: '核验完成,等待最早交卷时间' }); }
if (stage === 'confirming' || stage === 'submitting') {
const target = confirmationButton();
if (target.pending) return last;
if (target.reason) { disable(target.reason); return last; }
if (!entry.confirmed) {
entry.confirmed = true; entry.stage = stage = 'confirming'; await persist();
if (!canClick(token, ctx, true) || confirmationButton().button !== target.button) {
if (token === generation && enabled) disable('交卷确认前页面或核验条件已变化,请人工检查'); return last;
}
ownSubmit = true; try { target.button.click(); } finally { ownSubmit = false; }
log('已点击当前场次的交卷确认,等待网站结果');
}
return last;
}
if (!ctx.exam || !ctx.ready || runner !== 'complete') return last;
const findCommits = () => Array.from(ctx.root.querySelectorAll('.pc_examtest_commit')).filter(node => visible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true' && clean(node.textContent) === '交卷');
const commits = findCommits();
if (commits.length !== 1) { disable('当前交卷入口无法唯一识别'); return last; }
stage = entry.stage = 'submitting'; entry.submitAt = now(); entry.confirmed = false; await persist();
const currentCommits = findCommits();
if (!canClick(token, ctx, false) || currentCommits.length !== 1 || currentCommits[0] !== commits[0]) {
if (token === generation && enabled) disable('交卷前页面或核验条件已变化,请人工检查'); return last;
}
ownSubmit = true; try { commits[0].click(); } finally { ownSubmit = false; }
log('已点击交卷,等待当前场次的确认或结果');
return emit({ message: '交卷处理中,等待网站确认' });
} catch (failure) { disable('考试核验失败:' + failure.message); return last; }
finally { busy = false; }
}
function manual(event) {
if (!event.isTrusted || ownSubmit || !enabled) return;
const node = event.target?.nodeType === 1 ? event.target : event.target?.parentElement;
if (node?.closest('.pc_examtest_content_list_des,.pc_examtest_commit,.dialog_box button,[role="dialog"] button,.examine_box button')) disable('检测到人工处理,已取消自动交卷计划');
}
doc.addEventListener('click', manual, true);
return { observe, enable, disable, tick, getState: () => Object.assign({}, last, { enabled, stage }),
destroy() { generation++; observationVersion++; destroyed = true; enabled = false; doc.removeEventListener('click', manual, true); } };
}
const api = { create, readPaper, remainingSeconds, paperKey };
root.YianExamController = api;
if (typeof module !== 'undefined' && module.exports) module.exports = api;
})(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: '', panelPosition: null };
let snapshot = { records: [], counts: {}, total: 0, revision: '', storage: 'origin' };
let loading = true, disposed = false, scopedOut = 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();
let positionRevision = 0;
let learning = null, examController = null, learningState = {}, examState = {}, runnerState = 'idle';
let actionEpoch = 0;
let learningSettings = { mode: 'videoQuiz', execution: 'sequential', rate: 1, answerInterval: 4, minSubmitMinutes: 6, associations: {} };
const inScope = () => YianSiteScope.isAllowedURL(location.href);
function topicPreflight() {
return window.YianLearningEngine.preflightTopic(document, location.href, snapshot, learningSettings.associations || {});
}
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) : '';
const topic = route.mode === 'test' ? topicPreflight() : null;
return { ready: inScope() && !!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, submitted: route.submitted, expired: route.expired,
courses: topic && topic.status === 'matched' ? topic.courses : undefined,
topicKey: topic ? JSON.stringify([topic.status, topic.courses, learningSettings.associations]) : '' };
}
const runner = window.YianAnswerRunner.create({ getContext,
match: (question, context) => matcher.match(question, context, snapshot),
getInterval: () => learningSettings.answerInterval,
onProgress: progress => { metrics = progress; panel.setState({ metrics }); },
onState: (state, reason) => {
if (disposed) return;
runnerState = state;
if (state === 'paused') { config.autoAnswer = false; runKey = ''; panel.setState({ autoAnswer: false }); }
if (state === 'paused') { examController?.disable(reason); learning?.configure({ examPaused: true }); }
if (reason && (state === 'paused' || state === 'complete')) panel.setNotice(reason, state === 'paused' ? 'warning' : 'success');
}
});
function courses() {
return Array.from(new Set(snapshot.records.map(record => record.course))).sort();
}
function renderLearning() {
if (!learning) return;
const video = learningState.video ? Object.assign({}, learningState.video, { learnedTime: learningState.video.learnedSeconds,
isLearned: learningState.video.platformComplete, refreshCount: learningState.video.retryCount }) : null;
const route = routeInfo();
const currentExam = route.mode === 'test' && route.root ? Object.assign({}, learningState.exam || {}, examState) : learningState.exam || {};
panel.setState({ learning: Object.assign({}, learningSettings, learningState, {
video, exam: Object.assign({}, currentExam, { status: currentExam.stage || currentExam.status,
submitWaitSeconds: currentExam.waitSeconds }), associations: learningSettings.associations || {},
currentTitle: window.YianLearningEngine.describePage(document, location.href).title }) });
}
function refreshPanel(extra) {
panel.setState(Object.assign({ autoAnswer: config.autoAnswer, allowCopy: config.allowCopy,
exportEnabled: config.exportEnabled, position: config.panelPosition, role: config.role, course: config.course, courses: courses(),
bankTotal: snapshot.total, uniqueTotal: snapshot.questionTotal, conflictTotal: snapshot.conflictTotal,
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 }));
return result;
}
async function saveSetting(key, value) {
try { await store.setSetting(key, value); }
catch (error) { panel.setNotice('设置保存失败:' + error.message, 'error'); }
}
async function setRole(role) {
// Compatibility for old integrations; a role no longer filters any answers.
config.role = ''; return '';
}
function setAutoAnswer(enabled) {
if (!enabled) actionEpoch++;
const context = getContext();
if (enabled && (!context.ready || !(context.practice || context.exam) || context.local || !snapshot.total)) {
panel.setNotice('请先导入统一题库,并进入已加载完成的兼容练习或桌面考试页面。', 'warning'); refreshPanel(); return false;
}
if (enabled && context.mode === 'test' && topicPreflight().status !== 'matched') {
panel.setNotice('请先在题库栏确认当前考试标题与题库课程/主题的关联。', 'warning'); return false;
}
config.autoAnswer = !!enabled; runKey = '';
if (!enabled) { runner.stop('自动选择已关闭,已生效的选择保留。'); examController?.disable('自动选择已关闭'); }
refreshPanel(); tick(); return config.autoAnswer;
}
function setCopy(enabled) {
config.allowCopy = !!enabled; copy.setEnabled(config.allowCopy && inScope()); refreshPanel(); saveSetting('allowCopy', config.allowCopy);
}
async function importFiles(files) {
examController?.disable('题库正在更新');
if (learning) await learning.pause();
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 + ' 条来源记录,' + snapshot.questionTotal + ' 道题,' + snapshot.conflictTotal + ' 道存在答案冲突。', '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 (!inScope()) throw new Error('当前页面不在谊安学脚本生效范围内');
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,
onPositionChange: position => { positionRevision++; config.panelPosition = position; saveSetting('panelPosition', position); },
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
,onTaskAction: action => { taskAction(action).catch(error => panel.setNotice(error.message, 'error')); },
onLearningSetting: (key, value) => { changeLearningSetting(key, value).catch(error => panel.setNotice(error.message, 'error')); },
onLogAction: action => {
if (action === 'clear') learning?.clearLogs?.();
if (action === 'copy') {
const text = (learningState.logs || []).map(item => new Date(item.time).toLocaleTimeString() + ' [' + item.level + '] ' + item.message).join('\n');
if (typeof GM_setClipboard === 'function') GM_setClipboard(text); else navigator.clipboard?.writeText(text).catch(() => {});
}
}
});
async function changeLearningSetting(key, value) {
const allowed = ['mode', 'execution', 'rate', 'answerInterval', 'minSubmitMinutes', 'associations', 'stallSeconds', 'retryDelaySeconds', 'maxRefresh'];
if (!allowed.includes(key)) return;
if (key === 'mode' && !['video', 'videoQuiz', 'exam', 'all'].includes(value)) throw new Error('请选择有效任务模式');
if (key === 'execution' && !['sequential', 'parallel'].includes(value)) throw new Error('请选择顺序或并行');
const ranges = { rate: [1, 2], answerInterval: [0, 120], minSubmitMinutes: [0.5, 180], stallSeconds: [1, 600], retryDelaySeconds: [1, 120], maxRefresh: [1, 3] };
if (ranges[key]) {
const range = ranges[key]; value = Number(value);
if (!Number.isFinite(value) || value < range[0] || value > range[1] || key === 'maxRefresh' && !Number.isInteger(value)) {
throw new Error('设置数值须在 ' + range[0] + '~' + range[1] + ' 范围内');
}
}
if (key === 'associations' && (!value || typeof value !== 'object' || Array.isArray(value)
|| Object.values(value).some(courses => !Array.isArray(courses) || courses.some(course => typeof course !== 'string' || !course.trim())))) {
throw new Error('主题关联须使用完整标题及题库课程名称列表');
}
if (['mode', 'execution', 'associations'].includes(key)) {
actionEpoch++;
config.autoAnswer = false; runner.stop('执行模式或主题关联变更'); examController.disable('设置变更,须重新启动核验');
await learning.pause();
}
learningSettings = Object.assign({}, learningSettings, { [key]: value });
await learning.configure({ [key]: value });
await saveSetting('learningSettings', learningSettings);
refreshPanel(); renderLearning();
}
async function taskAction(action) {
actionEpoch++;
await ready;
if (!inScope()) return;
if (action === 'start' || action === 'resume') {
if (action === 'start') await learning.start(); else await learning.resume();
} else if (action === 'pause' || action === 'stop') {
config.autoAnswer = false; runner.stop('用户暂停或停止任务'); examController.disable('用户暂停或停止任务');
if (action === 'pause') await learning.pause(); else await learning.stop();
} else if (action === 'videoPause' || action === 'videoResume') await learning.configure({ videoPaused: action === 'videoPause' });
else if (action === 'examPause') { await learning.configure({ examPaused: true }); setAutoAnswer(false); }
else if (action === 'examResume') {
await learning.configure({ examPaused: false });
const epoch = actionEpoch;
if (await examController.enable() && epoch === actionEpoch) setAutoAnswer(true);
}
renderLearning();
}
const learningStorage = { get: (key, fallback) => store.getSetting('learning:' + key, fallback), set: (key, value) => store.setSetting('learning:' + key, value) };
examController = window.YianExamController.create({ getContext, getSnapshot: () => snapshot,
getTopic: topicPreflight, getSettings: () => learningSettings, getRunnerState: () => runnerState,
storage: learningStorage, onLog: (level, message) => learning?.log(level, message),
onState: state => { examState = state; renderLearning(); } });
learning = window.YianLearningEngine.create({
getSnapshot: () => ({ document, url: location.href, courses: courses(), bank: snapshot,
bankVersion: snapshot.revision, accountId: document.querySelector('[data-user-id]')?.getAttribute('data-user-id') || '', planId: '' }),
getExamState: () => examController.getState(),
onExamStart: async () => {
const epoch = actionEpoch;
if (await examController.enable() && epoch === actionEpoch && learning.getState().status === 'running'
&& !learning.getState().examPaused) return setAutoAnswer(true);
return false;
},
onExamPause: reason => { setAutoAnswer(false); examController.disable(reason); },
solveQuiz: async (_popup, quizContext) => {
const revision = snapshot.revision;
const mode = learningSettings.mode;
const epoch = actionEpoch;
const value = await window.YianQuizAdapter.solve({ document, snapshot, matcher,
context: { course: window.YianLearningEngine.describePage(document, location.href).title,
sourceUrl: location.href, answerInterval: quizContext.answerInterval },
getActive: () => epoch === actionEpoch && !disposed && inScope() && learning.getState().status === 'running'
&& !learning.getState().videoPaused && mode === learningSettings.mode && revision === snapshot.revision });
return Object.assign({}, value, { status: value.status === 'complete' ? 'completed' : value.status });
},
storage: learningStorage,
navigate: url => { if (YianSiteScope.isAllowedURL(url)) location.assign(url); else throw new Error('任务链接不在脚本范围内'); },
reload: () => location.reload(),
openWorker: url => {
if (!YianSiteScope.isAllowedURL(url)) throw new Error('任务链接不在脚本范围内');
if (typeof GM_openInTab === 'function') return GM_openInTab(url, { active: false, insert: true, setParent: true });
const child = window.open(url, '_blank'); if (!child) throw new Error('浏览器阻止并行标签页,请允许打开后重试'); return child;
},
onState: state => { learningState = state; renderLearning(); }
});
function tick() {
if (disposed) return;
if (!inScope()) {
if (!scopedOut) {
scopedOut = true;
config.autoAnswer = false; config.exportEnabled = false; runKey = '';
runner.stop('页面已离开脚本生效范围'); copy.setEnabled(false);
examController.disable('页面已离开脚本范围'); learning.pause();
panel.host.style.setProperty('display', 'none', 'important');
previous = page; page = null; metrics = emptyMetrics();
}
return;
}
if (scopedOut) {
scopedOut = false;
panel.host.style.setProperty('display', 'block', 'important');
copy.setEnabled(config.allowCopy);
}
const route = routeInfo();
if (!loading) {
examController.tick().catch(error => panel.setNotice(error.message, 'error'));
learning.tick().catch(error => panel.setNotice(error.message, 'error'));
}
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 + ' 题。按统一题库严格核对题干、选项和正确答案全文。'
+ (examController.getState().enabled ? '自动交卷条件已启用,请留意预计得分与倒计时。' : '仅选择答案;通过任务总览启动考试任务后才按条件自动交卷。');
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 && !!snapshot.total });
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 + '|' + snapshot.revision;
if (config.autoAnswer && snapshot.total && 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(); examController.destroy(); learning.destroy(); copy.destroy(); panel.destroy();
}
const ready = (async () => {
const positionRevisionAtRead = positionRevision;
try {
const values = await Promise.all([store.load(), store.getSetting('learningSettings', {}), store.getSetting('allowCopy', true), store.getSetting('panelPosition', null)]);
snapshot = values[0]; config.role = '';
if (snapshot.migration?.status === 'pending') panel.setNotice('旧题库仍可使用,但统一存储迁移尚未保存:' + snapshot.migration.error, 'warning');
learningSettings = Object.assign({}, learningSettings, values[1]); await learning.configure(learningSettings);
config.allowCopy = values[2] !== false;
const position = values[3];
// A drag during asynchronous storage loading wins over the older saved position.
if (positionRevision === positionRevisionAtRead) {
config.panelPosition = position && typeof position.left === 'number' && Number.isFinite(position.left)
&& typeof position.top === 'number' && Number.isFinite(position.top) ? { left: position.left, top: position.top } : null;
}
} catch (error) { panel.setNotice('题库读取失败:' + error.message + '。可重新导入题库。', 'error'); }
loading = false; copy.setEnabled(config.allowCopy && inScope()); refreshPanel(); tick(); return snapshot;
})();
const api = Object.freeze({ ready, importFiles, setRole, setAutoAnswer, setCopy, setExportEnabled, search: performSearch, getContext,
setLearningSetting: changeLearningSetting, taskAction, getLearningState: () => learning.getState(), getExamState: () => examController.getState(),
getSnapshot: () => snapshot, getState: () => ({ ...config, metrics, learning: learning.getState(), exam: examController.getState(), pageReady: !!page && page.ready }),
exportCurrent, collectCurrent: () => core.extract(document, routeInfo().sourceUrl), stop: dispose });
window.YianAssistant = api; window.YianQuestionBankExporter = api;
refreshPanel(); tick();
})();
})();