4524 lines
403 KiB
JavaScript
4524 lines
403 KiB
JavaScript
// ==UserScript==
|
||
// @name 谊安学助手 2.0
|
||
// @namespace yianxue-question-bank
|
||
// @version 2.1.2
|
||
// @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, manualConfirmIncomplete: true, autoContinuePlayback: true, skipSingleAttemptQuiz: true, resumePlaybackOnReturn: true, 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 learningSwitch(container, key, label, fallback) {
|
||
const control = makeSwitch(label, function () {
|
||
state.learning[key] = control.input.checked;
|
||
invoke('onLearningSetting', key, control.input.checked);
|
||
refreshLearning();
|
||
}, container);
|
||
control.input.setAttribute('data-testid', 'setting-' + key);
|
||
learningControls[key] = { input: control.input, fallback: fallback, switch: true };
|
||
return control.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');
|
||
videoModeNote.setAttribute('data-testid', 'video-mode-note'); views.video.appendChild(videoModeNote);
|
||
learningSetting(views.video, 'rate', '播放倍速', [[1, '1X'], [1.25, '1.25X'], [1.5, '1.5X'], [2, '2X(最高)']], 1);
|
||
learningSwitch(views.video, 'autoContinuePlayback', '自动继续上次播放', true);
|
||
views.video.appendChild(el(doc, 'p', 'hint', '运行中的视频任务出现“是否继续上次播放的位置”时,自动选择“继续播放”;关闭后由你处理。'));
|
||
learningSwitch(views.video, 'skipSingleAttemptQuiz', '仅 1 次答题机会时交给我处理', true);
|
||
views.video.appendChild(el(doc, 'p', 'hint', '含课中题和课程考试:网页明确显示仅允许或剩余 1 次机会时,不自动答题,等待你处理。次数未显示时不能推断;关闭后仍需满足原有题库核验条件。'));
|
||
learningSwitch(views.video, 'resumePlaybackOnReturn', '切回页面时自动恢复播放', true);
|
||
const playbackReturnHint = el(doc, 'p', 'hint', '只恢复切页引起的暂停。手动暂停优先;你暂停播放器、暂停任务,或有题目等待处理时,不会自动播放。');
|
||
playbackReturnHint.setAttribute('data-testid', 'playback-return-hint');
|
||
views.video.appendChild(playbackReturnHint);
|
||
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]);
|
||
learningSwitch(examIntro, 'manualConfirmIncomplete', '题目不全时手动确认', true);
|
||
const examInfo = el(doc, 'dl', 'info-grid');
|
||
const examMessage = el(doc, 'p', 'explanation');
|
||
const examThreshold = el(doc, 'p', 'threshold');
|
||
const incompleteHint = el(doc, 'p', 'hint');
|
||
examThreshold.setAttribute('data-testid', 'exam-threshold');
|
||
incompleteHint.setAttribute('data-testid', 'incomplete-confirmation-hint');
|
||
const examActions = el(doc, 'div', 'task-actions');
|
||
const examPause = taskButton(examActions, '暂停考试任务', 'examPause');
|
||
const examResume = taskButton(examActions, '继续考试任务', 'examResume');
|
||
examIntro.append(examInfo, examThreshold, examMessage, examActions, incompleteHint);
|
||
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: '播放中', backgroundPaused: '后台暂停,等待返回', 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 (setting.switch) setting.input.checked = learning[key] == null ? setting.fallback : learning[key] !== false;
|
||
else if (root.activeElement !== setting.input) setting.input.value = plain(learning[key] == null ? setting.fallback : learning[key]);
|
||
});
|
||
const manualConfirmIncomplete = learning.manualConfirmIncomplete !== false;
|
||
setText(examThreshold, manualConfirmIncomplete
|
||
? '必须整卷每题核验且预计得分严格大于 80 分;题目不全时由用户在原网页手动确认交卷。'
|
||
: '允许题库部分缺题时自动交卷:已核验预计得分严格大于 80 分,所有可匹配题均已核验,且已满最早交卷时间。');
|
||
setText(incompleteHint, (manualConfirmIncomplete
|
||
? '本开关已开启:存在未匹配、冲突或未核验题时,即使超过 80 分、已满交卷时长也不自动交卷;网页提示未答完时同样等待人工。'
|
||
: '本开关已关闭:仅已精确匹配、点击并核验完整答案文字的题目计分,其余题目不计分;满足得分、计时与当前试卷核验条件后可自动交卷。')
|
||
+ '切换会暂停当前计划,请在任务总览点击继续重新核验。无论开关状态,试卷未完整加载均禁止自动交卷;预计得分正好 80 分等待人工。练习与课中题不使用考试交卷门槛。');
|
||
const videoEnabled = learning.mode !== 'exam';
|
||
setText(videoModeNote, !videoEnabled ? '本模式未启用视频;可查看设置与记录。'
|
||
: learning.mode === 'video' ? '当前为“仅视频”:课中题由你处理,视频完成后不会进入独立或关联考试。若要视频完成后答题,请在任务总览选择“视频+考试”。'
|
||
: learning.mode === 'videoQuiz' ? '当前为“视频+课中答题”:只自动处理课中题,视频完成后不会进入独立或关联考试。若要视频完成后答题,请在任务总览选择“视频+考试”。'
|
||
: '当前为“视频+考试”:平台确认视频完成后,进入已开放的关联考试。' + (learning.skipSingleAttemptQuiz !== false ? '仅 1 次答题机会的人工保护仍生效。' : '单次机会保护已关闭,仍按题库核验及交卷条件执行。'));
|
||
videoModeNote.hidden = false;
|
||
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'], '尚未预检'))],
|
||
['全文核验', exam.total == null ? '待确认' : plain(exam.verifiedCount || 0) + ' / ' + plain(exam.total) + ' 题'],
|
||
['交卷确认', exam.submitted || exam.status === 'submitted' ? '网站已确认交卷' : exam.requiresManualSubmission ? '题目未全部核验或网站提示未答完,请在原网页手动确认' : manualConfirmIncomplete ? '整卷每题核验后才按条件自动交卷' : '已核验得分超过 80 分并满最早交卷时间后才按条件自动交卷'],
|
||
['预计得分', 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);
|
||
|
||
/* Visible attempt labels shared by course quizzes and course exams. */
|
||
(function (root) {
|
||
'use strict';
|
||
const IGNORE = [
|
||
'script', 'style', 'noscript', 'template', 'textarea', 'input', 'select', 'option', 'pre', 'code', 'svg', 'canvas',
|
||
'.examtest_content_question', '.examtest_content_list', '.examtest_content_list_des',
|
||
'.pc_examtest_content_question', '.pc_examtest_content_list',
|
||
'.model_answer_wrapper', '.model_answer', '.model_dec',
|
||
'[data-question-stem]', '[data-question-text]', '[data-question-options]', '[data-answer-text]',
|
||
'.question-stem', '.question-text', '.question-options', '.question-answer', '.option-text', '.answer-text', '[role="option"]'
|
||
].join(',');
|
||
const BLOCK = /^(?:ADDRESS|ARTICLE|ASIDE|BLOCKQUOTE|DD|DIV|DL|DT|FIELDSET|FIGCAPTION|FIGURE|FOOTER|FORM|H[1-6]|HEADER|HR|LI|MAIN|NAV|OL|P|SECTION|TABLE|TBODY|TD|TFOOT|TH|THEAD|TR|UL)$/;
|
||
const DIGITS = '[0-9零〇一二三四五六七八九十百两]+';
|
||
const TARGET = '(?:答题|作答|考试)';
|
||
const CAPTION = '(?:允许|允许的|可用|可|总|总共|最多|最大|剩余|还剩|还有|还可)';
|
||
const SEPARATOR = '(?:[::=]|为|是)?';
|
||
const LIMIT = '(?:仅|只有|仅有|只限)?';
|
||
const UNIT = '(?:次|[((]次[))])?';
|
||
const forms = [
|
||
new RegExp('^(?:本次|本场)?' + CAPTION + '(?:可|允许)?' + TARGET + '(?:总|剩余)?(?:次数|机会)' + SEPARATOR + LIMIT + '(' + DIGITS + ')' + UNIT + '$'),
|
||
new RegExp('^(?:本次|本场)?' + TARGET + '(?:允许|可用|总|最多|最大|剩余|还剩)(?:次数|机会)' + SEPARATOR + LIMIT + '(' + DIGITS + ')' + UNIT + '$'),
|
||
new RegExp('^(?:本次|本场)?' + TARGET + '(?:次数|机会)' + SEPARATOR + '(?:上限|最多|最大|仅剩|还剩|剩余)' + SEPARATOR + LIMIT + '(' + DIGITS + ')' + UNIT + '$'),
|
||
new RegExp('^(?:本次|本场)?(?:仅|只)(?:有|可|能|允许)?' + TARGET + '(' + DIGITS + ')次$'),
|
||
new RegExp('^(?:本次|本场)?(?:最多|总共|总|允许|还可|剩余)(?:可)?' + TARGET + '(' + DIGITS + ')次$'),
|
||
new RegExp('^(?:本次|本场)?(?:仅|只)(?:有|剩)?(' + DIGITS + ')次' + TARGET + '机会$'),
|
||
new RegExp('^(?:本次|本场)?(?:剩余|还剩|还有)(' + DIGITS + ')次' + TARGET + '机会$'),
|
||
new RegExp('^(?:本次|本场)?还可(?:补考|重考)' + SEPARATOR + '(' + DIGITS + ')次$'),
|
||
// The saved site's showButton2 label is “不及格,还可补考 N 次”.
|
||
// The template may wrap it in parentheses after the normal retake button.
|
||
new RegExp('^不及格[,,]还可补考(' + DIGITS + ')次$'),
|
||
new RegExp('^(?:重新答题|重新考试)?[((]不及格[,,]还可补考(' + DIGITS + ')次[))]$')
|
||
];
|
||
function clean(value) {
|
||
return String(value == null ? '' : value).replace(/[\u200b-\u200d\u2060\uFEFF]/g, '').replace(/[0-9]/g, char => String(char.charCodeAt(0) - 0xff10)).replace(/\s+/g, ' ').trim();
|
||
}
|
||
function visible(node) {
|
||
if (!node || !node.isConnected || node.closest('[hidden],[aria-hidden="true"],[inert]')) return false;
|
||
const view = node.ownerDocument && node.ownerDocument.defaultView;
|
||
if (!view) return false;
|
||
for (let current = node; current && current.nodeType === 1; current = current.parentElement) {
|
||
const style = view.getComputedStyle(current);
|
||
if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse' || Number(style.opacity) === 0) return false;
|
||
}
|
||
return node.getClientRects().length > 0 || view.getComputedStyle(node).display === 'contents';
|
||
}
|
||
function numberValue(raw) {
|
||
if (/^[0-9]+$/.test(raw)) return Number(raw);
|
||
if (/^[零〇]+$/.test(raw)) return 0;
|
||
if (raw === '一') return 1;
|
||
// Only zero and one affect this policy. Other explicit Chinese counts still
|
||
// form part of the signature but are never mistaken for a single attempt.
|
||
return 2;
|
||
}
|
||
function evidenceText(scope) {
|
||
const candidates = new Set();
|
||
function walk(node) {
|
||
if (node.nodeType === 3) return node.textContent;
|
||
if (node.nodeType !== 1 || node.matches(IGNORE) || !visible(node)) return '';
|
||
if (node.tagName === 'BR') return '\n';
|
||
const text = Array.from(node.childNodes, walk).join(' ');
|
||
// Small DOM rows permit labels and values in separate inline spans while
|
||
// whole pages and unrelated paragraphs are never treated as one label.
|
||
if (text.length <= 240) candidates.add(clean(text));
|
||
return BLOCK.test(node.tagName) ? '\n' + text + '\n' : text;
|
||
}
|
||
const lines = walk(scope).split(/[\n,,;;。!!]+/).map(clean).filter(Boolean);
|
||
lines.forEach(line => { if (line.length <= 240) candidates.add(line); });
|
||
// Definition lists commonly render a status label and its value separately.
|
||
scope.querySelectorAll('dt').forEach(label => {
|
||
const value = label.nextElementSibling;
|
||
if (value && value.tagName === 'DD' && visible(label) && visible(value) && !label.closest(IGNORE) && !value.closest(IGNORE)) {
|
||
candidates.add(clean(walk(label) + ' ' + walk(value)));
|
||
}
|
||
});
|
||
return candidates;
|
||
}
|
||
function read(scope) {
|
||
const empty = { blocked: false, known: false, count: null, reason: '', signature: '[]' };
|
||
// The caller must pass the current exam/quiz container. Reading Document or
|
||
// body could borrow another course's limits or a hidden background dialog.
|
||
if (!scope || scope.nodeType !== 1 || /^(?:HTML|BODY)$/.test(scope.tagName) || !visible(scope) || scope.closest(IGNORE)) return empty;
|
||
const evidence = new Set();
|
||
evidenceText(scope).forEach(raw => {
|
||
const value = raw.replace(/\s+/g, '').replace(/[::]$/, '');
|
||
for (const expression of forms) {
|
||
const match = value.match(expression);
|
||
if (!match) continue;
|
||
evidence.add(JSON.stringify([value, numberValue(match[1])]));
|
||
break;
|
||
}
|
||
});
|
||
const labels = Array.from(evidence).sort();
|
||
const values = labels.map(item => JSON.parse(item)[1]);
|
||
const count = values.includes(0) ? 0 : values.includes(1) ? 1 : null;
|
||
return {
|
||
blocked: count !== null,
|
||
known: labels.length > 0,
|
||
count: count,
|
||
reason: count === 0 ? '网页明确提示已无可用答题或考试次数,等待人工处理' : count === 1 ? '网页明确提示仅允许或剩余 1 次答题或考试机会,等待人工处理' : '',
|
||
signature: JSON.stringify(labels.map(item => JSON.parse(item)))
|
||
};
|
||
}
|
||
const api = { read: read };
|
||
if (typeof module === 'object' && module.exports) module.exports = api;
|
||
if (root) root.YianAttemptPolicy = api;
|
||
})(typeof window === 'object' ? window : typeof globalThis === 'object' ? globalThis : this);
|
||
|
||
/* 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';
|
||
const POLICY_KEYS = ['manualConfirmIncomplete', 'autoContinuePlayback', 'resumePlaybackOnReturn', 'skipSingleAttemptQuiz'];
|
||
|
||
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 examIdentity(value) {
|
||
try { const url = new URL(value); const match = url.pathname.match(/^\/clientcn\/(?:exam|examtest)\/([^/]+)(?:\/questions)?\/?$/); return match ? url.origin + ':exam:' + match[1] : ''; } catch (_) { return ''; }
|
||
}
|
||
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 playbackPromptOf(doc) {
|
||
// 原站 ngx-bootstrap 将模板放在 body portal,app-dialog-box 自身可能为空。
|
||
const bodies = Array.from(doc.querySelectorAll('.modal-dialog.dialog_box .modal-body,[role="dialog"] .modal-body'))
|
||
.filter(node => visible(node) && /^是否继续上次播放的位置[??]$/.test(clean(node.textContent)));
|
||
const candidates = [];
|
||
for (const body of bodies) {
|
||
const container = body.closest('.modal-content,[role="dialog"]'); if (!container) continue;
|
||
if (candidates.some(item => item.container === container)) continue;
|
||
const buttons = Array.from(container.querySelectorAll('.modal-footer button')).filter(node => visible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true');
|
||
const keep = buttons.filter(node => clean(node.textContent) === '继续播放');
|
||
const restart = buttons.filter(node => clean(node.textContent) === '重新开始');
|
||
candidates.push({ container, body, button: keep.length === 1 && restart.length === 1 ? keep[0] : null });
|
||
}
|
||
return candidates.length ? candidates.length === 1 ? candidates[0] : { ambiguous: true } : 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 visibilityPlayback = null; let returnResumeRequested = false; let pageAway = false; let playbackPromptClick = null;
|
||
let sharedPaused = false;
|
||
let manualConfirmRevision = 0; let pendingManualConfirm = null;
|
||
let policySessionId = '', policyReadSerial = 0;
|
||
const authoritativePolicies = new Set(), policyAppliedReads = new Map(), policyLocalWrites = new Map();
|
||
const config = { mode: 'all', execution: 'sequential', rate: 1, answerInterval: 4, minSubmitMinutes: 6, manualConfirmIncomplete: true, autoContinuePlayback: true, resumePlaybackOnReturn: true, skipSingleAttemptQuiz: true, 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, manualConfirmIncomplete: true, autoContinuePlayback: true, resumePlaybackOnReturn: true, skipSingleAttemptQuiz: true, 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' && !pageAway && watchedDocument?.visibilityState !== 'hidden' && interactionAt > lastAutoPlayAt && now() - interactionAt < 2000) {
|
||
manualVideoPause = true; visibilityPlayback = null; returnResumeRequested = false;
|
||
}
|
||
}
|
||
function rememberPlayback() {
|
||
const snap = snapshot(); if (!snap.document || !safeURL(snap.url, snap.url)) return;
|
||
const task = state.tasks.find(t => t.id === activeTaskId); const page = describePage(snap.document, snap.url);
|
||
if (state.status !== 'running' || config.videoPaused || manualVideoPause || !task || task.kind !== 'video'
|
||
|| !['loading', 'playing', 'retrying'].includes(task.status) || !page.video || page.quiz || playbackPromptOf(snap.document) || page.video.ended || page.platformComplete) return;
|
||
if (!visibilityPlayback && (playStarted || !page.video.paused)) visibilityPlayback = { taskId: task.id, url: page.url, video: page.video, generation };
|
||
lastAdvancedAt = now(); attemptedAt = 0;
|
||
}
|
||
function onPageHidden() { pageAway = true; rememberPlayback(); }
|
||
function onPageReturn() {
|
||
if (watchedDocument && watchedDocument.visibilityState === 'hidden') return;
|
||
pageAway = false; lastAdvancedAt = now(); attemptedAt = 0;
|
||
returnResumeRequested = Boolean(visibilityPlayback && config.resumePlaybackOnReturn);
|
||
// 仍走正常调度、租约和弹题检查,事件本身不直接点击或播放。
|
||
root.setTimeout(() => { if (!destroyed) tick().catch(() => {}); }, 0);
|
||
}
|
||
function onVisibilityChange() {
|
||
if (watchedDocument && watchedDocument.visibilityState === 'hidden') onPageHidden(); else onPageReturn();
|
||
}
|
||
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 });
|
||
for (const key of POLICY_KEYS) state[key] = config[key];
|
||
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 || ''); }
|
||
function readExamAttempt(task, snap, page) {
|
||
const scope = snap.document.querySelector('app-examdetail,app-examtest,.pc_examtest');
|
||
const live = root.YianAttemptPolicy?.read(scope) || { known: false, blocked: false, count: null, reason: '', signature: '[]' };
|
||
if (!['exam', 'paper'].includes(page.kind)) return live;
|
||
const taskURL = task && (task.examPaperURL || task.url || task.pageUrl);
|
||
const sameExam = taskURL && examIdentity(taskURL) && examIdentity(taskURL) === examIdentity(page.url);
|
||
if (task && task.kind === 'exam' && (canonical(taskURL) === page.url || sameExam)) {
|
||
if (live.known) { task.attemptPolicy = clone(live); task.attemptPolicyURL = page.url; }
|
||
// 同一任务从详情进入试卷后,次数提示可能不再显示;保留已核对的证据。
|
||
else if (task.attemptPolicy && (task.attemptPolicyURL === page.url || sameExam)) return clone(task.attemptPolicy);
|
||
}
|
||
return live;
|
||
}
|
||
function getAttemptPolicy() {
|
||
const snap = snapshot(); if (!snap.document) return { known: false, blocked: false, count: null, reason: '', signature: '[]' };
|
||
return readExamAttempt(state.tasks.find(t => t.id === activeTaskId), snap, describePage(snap.document, snap.url));
|
||
}
|
||
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;
|
||
await syncPolicies(currentControl); session.config = clone(config);
|
||
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 conflictingExam = current && task.kind === 'exam' && examIdentity(current.examPaperURL || current.url || current.pageUrl)
|
||
&& examIdentity(task.url || task.pageUrl) && examIdentity(current.examPaperURL || current.url || current.pageUrl) !== examIdentity(task.url || task.pageUrl);
|
||
// 开考前详情路由与试卷路由不同;同一已授权转场不再生成第二场考试。
|
||
const alias = task.current && current && current.kind === task.kind &&
|
||
(task.kind === 'exam' && !conflictingExam && (current.enterClickedAt || current.examLinkClickedAt || current.navigationRequestedAt) && task.title === current.title
|
||
&& (!current.examPaperURL || current.examPaperURL === task.url)
|
||
&& (!current.enterClickedAt || now() - current.enterClickedAt < 60000 || current.examPaperURL === task.url)
|
||
|| 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
|
||
|| task.current && task.kind === t.kind && task.url && (canonical(t.examPaperURL || t.url) === canonical(task.url)
|
||
|| task.kind === 'exam' && examIdentity(t.examPaperURL || t.url || t.pageUrl) && examIdentity(t.examPaperURL || t.url || t.pageUrl) === examIdentity(task.url))) || (alias ? current : null);
|
||
if (!old) state.tasks.push(task);
|
||
else {
|
||
if (alias && task.url) old.url = task.url;
|
||
if (task.current && task.kind === 'exam' && /\/questions\/?$/.test(new URL(task.url).pathname)) old.examPaperURL = task.url;
|
||
if (task.status === 'completed') {
|
||
old.status = 'completed'; old.progress = 1; old.reason = '平台确认已完成';
|
||
if (old.kind === 'video' && state.video?.taskId === old.id) Object.assign(state.video, { platformComplete: true, status: 'completed' });
|
||
}
|
||
}
|
||
});
|
||
}
|
||
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);
|
||
watchedDocument.addEventListener('visibilitychange', onVisibilityChange, true);
|
||
if (watchedDocument.defaultView) { watchedDocument.defaultView.addEventListener('blur', onPageHidden); watchedDocument.defaultView.addEventListener('focus', onPageReturn); }
|
||
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); }
|
||
if (session) await syncPolicies(await storage.get(CONTROL_KEY));
|
||
mergeTasks(discoverTasks(snap.document, snap.url)); notify();
|
||
}
|
||
function syncManualConfirmation(shared) {
|
||
if (!session || !shared || shared.sessionId !== session.id || !shared.config || pendingManualConfirm) return;
|
||
const revision = Number(shared.manualConfirmRevision) || 0;
|
||
if (revision < manualConfirmRevision) return;
|
||
for (const key of POLICY_KEYS) if (!authoritativePolicies.has(key) && typeof shared.config[key] === 'boolean') { config[key] = shared.config[key]; state[key] = config[key]; }
|
||
manualConfirmRevision = revision; session.config = Object.assign({}, session.config);
|
||
for (const key of POLICY_KEYS) session.config[key] = config[key];
|
||
}
|
||
function ensurePolicySession() {
|
||
const id = session?.id || '';
|
||
if (policySessionId === id) return;
|
||
policySessionId = id; authoritativePolicies.clear(); policyAppliedReads.clear(); policyLocalWrites.clear();
|
||
}
|
||
function policyKey(key, id) { return 'policy:' + (id || session.id) + ':' + key; }
|
||
async function syncPolicies(shared) {
|
||
if (!session) return;
|
||
ensurePolicySession();
|
||
syncManualConfirmation(shared);
|
||
const id = session.id, serial = ++policyReadSerial;
|
||
const writes = new Map(POLICY_KEYS.map(key => [key, policyLocalWrites.get(key) || 0]));
|
||
const records = await Promise.all(POLICY_KEYS.map(key => storage.get(policyKey(key, id))));
|
||
if (!session || session.id !== id || destroyed) return;
|
||
records.forEach((record, index) => {
|
||
const key = POLICY_KEYS[index];
|
||
if (!record || record.sessionId !== id || typeof record.value !== 'boolean'
|
||
|| pendingManualConfirm?.values && key in pendingManualConfirm.values
|
||
|| (policyLocalWrites.get(key) || 0) !== writes.get(key)
|
||
|| serial < (policyAppliedReads.get(key) || 0)) return;
|
||
// 独立键是共享策略的权威值;旧 control 全量快照不能覆盖它。
|
||
authoritativePolicies.add(key); policyAppliedReads.set(key, serial);
|
||
config[key] = record.value; state[key] = record.value;
|
||
});
|
||
session.config = Object.assign({}, session.config);
|
||
for (const key of POLICY_KEYS) session.config[key] = config[key];
|
||
}
|
||
async function control(status, force, explicitWorkerAction, manualPatch) {
|
||
if (!session || worker && !explicitWorkerAction) return;
|
||
const controlEpoch = generation, controlSessionId = session.id;
|
||
ensurePolicySession();
|
||
const current = await storage.get(CONTROL_KEY);
|
||
if (!force && current && current.sessionId !== session.id) return;
|
||
const sameSession = current && current.sessionId === session.id;
|
||
if (manualPatch) {
|
||
if (sameSession && current.config) for (const key of POLICY_KEYS) {
|
||
if (!(key in manualPatch.values) && !authoritativePolicies.has(key) && typeof current.config[key] === 'boolean') config[key] = current.config[key];
|
||
}
|
||
Object.assign(config, manualPatch.values);
|
||
manualConfirmRevision = Math.max(manualConfirmRevision, sameSession ? Number(current.manualConfirmRevision) || 0 : 0) + 1;
|
||
const id = session.id;
|
||
await Promise.all(Object.entries(manualPatch.values).map(async ([key, value]) => {
|
||
authoritativePolicies.add(key);
|
||
await storage.set(policyKey(key, id), { sessionId: id, value, revision: manualConfirmRevision, updatedAt: now(), owner });
|
||
}));
|
||
}
|
||
await syncPolicies(current);
|
||
// 策略通过独立键发布,不将旧 control 快照写回,避免覆盖另一页刚发出的暂停。
|
||
if (manualPatch && sameSession || controlEpoch !== generation || session?.id !== controlSessionId) return;
|
||
const sharedConfig = clone(worker && sameSession ? current.config : config);
|
||
for (const key of POLICY_KEYS) sharedConfig[key] = config[key];
|
||
// 设置修改只发布策略,不得把另一页已经暂停的任务重新改成 running。
|
||
await storage.set(CONTROL_KEY, { sessionId: session.id, status: manualPatch && sameSession ? current.status : status, config: sharedConfig,
|
||
manualConfirmRevision, updatedAt: now(), owner: worker && sameSession ? current.owner : owner });
|
||
}
|
||
async function configure(patch, restoreOptions) {
|
||
await initialize(); patch = patch || {};
|
||
if (restoreOptions?.restore === true && session) {
|
||
await syncPolicies(await storage.get(CONTROL_KEY));
|
||
// 普通设置快照可能来自另一个标签的旧全量写回;重载只恢复尚无权威记录的策略。
|
||
patch = Object.assign({}, patch);
|
||
for (const key of POLICY_KEYS) if (authoritativePolicies.has(key)) delete patch[key];
|
||
}
|
||
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;
|
||
const policies = Object.fromEntries(POLICY_KEYS.filter(key => typeof patch[key] === 'boolean').map(key => [key, patch[key]]));
|
||
const manualPatch = Object.keys(policies).length ? { values: policies } : null;
|
||
if (manualPatch) {
|
||
ensurePolicySession(); pendingManualConfirm = manualPatch; Object.assign(config, policies);
|
||
for (const key of Object.keys(policies)) policyLocalWrites.set(key, (policyLocalWrites.get(key) || 0) + 1);
|
||
}
|
||
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('考试任务已局部暂停');
|
||
try { await control(state.status, false, Boolean(manualPatch), manualPatch); }
|
||
finally { if (pendingManualConfirm === manualPatch) pendingManualConfirm = null; }
|
||
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();
|
||
visibilityPlayback = null; returnResumeRequested = false;
|
||
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(); const pauseEpoch = ++generation; visibilityPlayback = null; returnResumeRequested = false; state.status = 'paused'; sharedPaused = false; 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);
|
||
if (pauseEpoch !== generation || state.status !== 'paused') return getState();
|
||
// 暂停写入完成后才接受远端“继续”;使写入期间读到旧 running 的轮询失效。
|
||
sharedPaused = true; generation++;
|
||
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++; visibilityPlayback = null; returnResumeRequested = false; 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; visibilityPlayback = null; returnResumeRequested = 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', '正在通过网页现有入口打开任务');
|
||
task.navigationRequestedAt = now();
|
||
if (node && task.kind === 'exam') task.examLinkClickedAt = now();
|
||
await persist();
|
||
const targetURL = task.url || (canonical(task.pageUrl) !== page.url ? task.pageUrl : '');
|
||
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 });
|
||
const playbackPrompt = playbackPromptOf(snap.document);
|
||
if (playbackPrompt && !popup) {
|
||
lastAdvancedAt = now(); attemptedAt = 0; visibilityPlayback = null; returnResumeRequested = false; state.video.status = 'waiting';
|
||
if (manualVideoPause || !config.autoContinuePlayback || playbackPrompt.ambiguous || !playbackPrompt.button) {
|
||
taskState(task, 'waiting', manualVideoPause ? '用户已暂停,请手动选择续播并继续任务' : !config.autoContinuePlayback ? '自动续播已关闭,请在网页选择继续播放或重新开始后继续任务' : '续播弹窗无法唯一确认,请人工选择后继续'); return;
|
||
}
|
||
if (playbackPromptClick && playbackPromptClick.container === playbackPrompt.container) {
|
||
if (now() - playbackPromptClick.at > 15000) taskState(task, 'waiting', '已选择继续播放,但网页弹窗未关闭,请人工检查');
|
||
return;
|
||
}
|
||
if (epoch !== generation || state.status !== 'running' || config.videoPaused || !config.autoContinuePlayback) return;
|
||
playbackPromptClick = { container: playbackPrompt.container, at: now() }; playStarted = false; lastTime = null;
|
||
taskState(task, 'loading', '已选择继续上次位置,等待网页恢复播放器'); playbackPrompt.button.click(); return;
|
||
}
|
||
if (!playbackPrompt) playbackPromptClick = null;
|
||
if (popup) {
|
||
visibilityPlayback = null; returnResumeRequested = false;
|
||
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, skipSingleAttemptQuiz: config.skipSingleAttemptQuiz, 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 ((page.hidden || pageAway) && video.paused && (playStarted || visibilityPlayback)) {
|
||
rememberPlayback(); lastAdvancedAt = now(); attemptedAt = 0;
|
||
state.video.status = 'backgroundPaused'; taskState(task, 'playing', '网站在后台暂停播放,等待切回页面后恢复'); return;
|
||
}
|
||
if (returnResumeRequested) {
|
||
const previous = visibilityPlayback; returnResumeRequested = false; visibilityPlayback = null;
|
||
if (previous && previous.taskId === task.id && previous.url === page.url && previous.video === video && previous.generation === epoch && config.resumePlaybackOnReturn) {
|
||
lastAdvancedAt = now(); attemptedAt = 0; lastTime = null;
|
||
if (video.paused) { playStarted = false; taskState(task, 'playing', '已切回页面,正在继续原播放位置'); }
|
||
}
|
||
} else if (visibilityPlayback && !page.hidden && !pageAway && video.paused && !config.resumePlaybackOnReturn) {
|
||
state.video.status = 'waiting'; taskState(task, 'waiting', '切回页面自动恢复已关闭,请手动播放后继续任务'); 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 observeExamState(task, readOnly) {
|
||
// 仅观察已经交给本场控制器的考试,避免将旧场次结果附到尚未开考的任务。
|
||
if (!task.examRequested) return;
|
||
const exam = typeof options.getExamState === 'function' ? options.getExamState() : null;
|
||
if (!exam) return;
|
||
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 (!readOnly && ['waiting', 'paused', 'failed', 'unknown', 'awaitingAttempt'].includes(stage)) taskState(task, 'waiting', exam.reason || exam.message || '考试等待人工处理');
|
||
else if (!readOnly && stage === 'waitingSubmit') taskState(task, 'waitingSubmit', exam.reason || '已核验,等待最早交卷时间');
|
||
}
|
||
async function examTick(task, snap, page) {
|
||
if (['exam', 'paper'].includes(page.kind)) {
|
||
const sourceURL = task.examPaperURL || task.url || task.pageUrl;
|
||
const sameURL = canonical(sourceURL) === page.url || examIdentity(sourceURL) && examIdentity(sourceURL) === examIdentity(page.url);
|
||
if (!sameURL) { taskState(task, 'waiting', '当前考试与已领取任务的地址不同,请核对后开始新一轮'); return; }
|
||
if (page.kind === 'paper' && task.enterClickedAt && !task.examPaperURL && page.title === task.title) task.examPaperURL = page.url;
|
||
}
|
||
const attempts = readExamAttempt(task, snap, page);
|
||
if (config.skipSingleAttemptQuiz && attempts.blocked && !task.examRequested) {
|
||
state.exam = { taskId: task.id, title: task.title, stage: 'waiting', requiresManualSubmission: true, message: attempts.reason };
|
||
taskState(task, 'waiting', attempts.reason); return;
|
||
}
|
||
// 人工交卷后仍需接收网站确认;等待/局部暂停期间不得重新开考或启动答题。
|
||
if (task.status === 'waiting' || config.examPaused) {
|
||
await observeExamState(task, true);
|
||
if (task.status !== 'completed' && 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', '考试控制器尚未接入,请人工处理');
|
||
}
|
||
await observeExamState(task, false);
|
||
}
|
||
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) {
|
||
const controlValue = await storage.get(CONTROL_KEY);
|
||
if (epoch !== generation) return getState();
|
||
if (await observeControl(controlValue)) return getState();
|
||
if (worker && controlValue && controlValue.sessionId === session.id && controlValue.config) {
|
||
const sharedConfig = Object.assign({}, controlValue.config); for (const key of POLICY_KEYS) delete sharedConfig[key];
|
||
Object.assign(config, sharedConfig);
|
||
}
|
||
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(); visibilityPlayback = null; returnResumeRequested = false; playbackPromptClick = null; }
|
||
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') { await persist(); 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.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) return false;
|
||
const controlEpoch = generation;
|
||
const shared = value === undefined ? await storage.get(CONTROL_KEY) : value;
|
||
await syncPolicies(shared);
|
||
if (controlEpoch !== generation) return true;
|
||
if (state.status !== 'running' && !sharedPaused) return false;
|
||
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);
|
||
watchedDocument.removeEventListener('visibilitychange', onVisibilityChange, true);
|
||
if (watchedDocument.defaultView) { watchedDocument.defaultView.removeEventListener('blur', onPageHidden); watchedDocument.defaultView.removeEventListener('focus', onPageReturn); }
|
||
}
|
||
if (typeof options.onExamPause === 'function') options.onExamPause('学习控制器已退出'); release().catch(() => {});
|
||
}
|
||
return { configure, start, pause, resume, stop, tick, getState, getAttemptPolicy, 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)));
|
||
}
|
||
function attemptPolicy(view) {
|
||
if (!view?.container) return null;
|
||
const scope = view.container.closest(OWNER) || view.container;
|
||
return root.YianAttemptPolicy?.read?.(scope) || null;
|
||
}
|
||
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, attemptBlocked = null;
|
||
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 attemptAllowed = () => {
|
||
if (context.skipSingleAttemptQuiz === false) return true;
|
||
if (!root.YianAttemptPolicy || typeof root.YianAttemptPolicy.read !== 'function') {
|
||
attemptBlocked = '答题次数核验组件未就绪,已停止自动操作,请人工处理';
|
||
return false;
|
||
}
|
||
const policy = attemptPolicy(detect(doc));
|
||
if (!policy?.blocked) return true;
|
||
attemptBlocked = policy.reason || '网页提示课中题答题机会受限,已停止自动操作,请人工处理';
|
||
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() || !attemptAllowed() || !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', '课中答题已取消,等待重新启动');
|
||
if (!attemptAllowed()) return result('waiting', attemptBlocked, { manualRequired: true, singleAttempt: true });
|
||
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 (!attemptAllowed()) return result('waiting', attemptBlocked, { manualRequired: true, singleAttempt: true });
|
||
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', attemptBlocked || '题目或任务状态已变化,停止课中答题', attemptBlocked ? { manualRequired: true, singleAttempt: true } : null);
|
||
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', attemptBlocked || '课中题提交入口无法唯一确认', attemptBlocked ? { manualRequired: true, singleAttempt: true } : null);
|
||
// 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', attemptBlocked || '课中题已判定正确,但下一题或关闭按钮无法唯一确认', attemptBlocked ? { manualRequired: true, singleAttempt: true } : null);
|
||
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;
|
||
const fullyVerified = complete && matchedCount === bank.total && verifiedCount === bank.total;
|
||
return { title: bank.title, total: bank.total, complete, totalScore, predictedScore, matchedCount, verifiedCount,
|
||
fullyVerified, unverifiedCount: Math.max(0, bank.total - 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 manualIncomplete() { return settings().manualConfirmIncomplete !== false; }
|
||
function attemptPolicy() {
|
||
const policy = (options.getAttemptPolicy ? options.getAttemptPolicy() : root.YianAttemptPolicy?.read(doc.querySelector('app-examdetail,app-examtest,.pc_examtest'))) || {};
|
||
const active = settings().skipSingleAttemptQuiz !== false;
|
||
return { active, blocked: active && policy.blocked === true, count: policy.count == null ? null : policy.count,
|
||
reason: clean(policy.reason) || '仅有 1 次答题机会,已停止自动答题与交卷,请在原网页人工操作',
|
||
signature: active ? JSON.stringify([policy.signature || '', policy.blocked === true, policy.count == null ? null : policy.count]) : '' };
|
||
}
|
||
function blockSingleAttempt(policy) {
|
||
generation++; enabled = false;
|
||
if (['submitting', 'confirming', 'unknown'].includes(stage)) stage = 'unknown';
|
||
else stage = 'waiting';
|
||
error = policy.reason;
|
||
return emit({ skipSingleAttemptQuiz: policy.active, singleAttemptBlocked: true, attemptCount: policy.count,
|
||
requiresManualSubmission: true, message: error });
|
||
}
|
||
function scopeValue(ctx, evidence, topic) {
|
||
const policy = attemptPolicy();
|
||
return JSON.stringify([keyFor(ctx), ctx.revision, evidence.signature, topic?.courses, manualIncomplete(), policy.active, policy.signature]);
|
||
}
|
||
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;
|
||
const initialPolicy = attemptPolicy();
|
||
if (initialPolicy.blocked) { blockSingleAttempt(initialPolicy); return false; }
|
||
await observe();
|
||
if (token !== generation || !sameContext(requested, context()) || !entry || ['submitting', 'confirming', 'unknown'].includes(stage) || destroyed || resultVisible()) return false;
|
||
const currentPolicy = attemptPolicy();
|
||
if (currentPolicy.blocked) { blockSingleAttempt(currentPolicy); return false; }
|
||
if (entry.manualRequired && stage !== 'awaitingAttempt' && manualIncomplete()) {
|
||
stage = 'waiting'; error = '网站提示存在未作答题,请在原网页手动确认交卷';
|
||
emit({ requiresManualSubmission: true, message: error }); 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 (Array.from(doc.querySelectorAll('.dialog_box,[role="dialog"],.modal')).some(visible)) {
|
||
emit({ message: '请先在网页取消或关闭当前交卷对话框,再继续任务' }); return false;
|
||
}
|
||
if (stage === 'awaitingAttempt') {
|
||
const timer = timerEvidence(ctx);
|
||
entry = { startedAt: timer.startedAt, initialRemaining: timer.remain, lastRemaining: timer.remain, lastObservedAt: timer.stamp, stage: 'idle' };
|
||
}
|
||
if (!manualIncomplete() && entry.manualRequired) { entry.manualRequired = false; entry.confirmed = false; delete entry.submitAt; }
|
||
scope = scopeValue(ctx, evidence, topic);
|
||
enabled = true; stage = 'answering'; error = ''; entry.stage = stage; entry.paperSignature = evidence.signature; await persist();
|
||
const finalPolicy = attemptPolicy();
|
||
if (token === generation && finalPolicy.blocked) { blockSingleAttempt(finalPolicy); return false; }
|
||
if (token !== generation || destroyed || !enabled || !sameContext(ctx, context()) || !context().ready || options.getTopic()?.status !== 'matched' || scope !== scopeFor(context())) {
|
||
if (token === generation) disable('启动过程中页面、题库或任务状态已变化,请重新启动'); return false;
|
||
}
|
||
log(manualIncomplete() ? '当前考试已授权:题目不全时由用户在网页确认;整卷核验后按得分和时长交卷'
|
||
: '题目不全时手动确认已关闭:全部可匹配题核验后,严格大于 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 scopeValue(ctx, evidence, topic);
|
||
}
|
||
function canClick(token, ctx, confirmation) {
|
||
if (token !== generation || destroyed || !enabled || !sameContext(ctx, context())) return false;
|
||
const policy = attemptPolicy();
|
||
if (policy.blocked) { blockSingleAttempt(policy); 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 === scopeValue(current, evidence, topic)
|
||
&& evidence.scoreValid && evidence.matchedCount === evidence.verifiedCount
|
||
&& (!manualIncomplete() || evidence.fullyVerified && !entry.manualRequired) && 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 };
|
||
if (manualIncomplete() && containers.some(node => /未答|未作答|没有作答|尚未作答|漏答|未完成.*题|题.*未完成/.test(clean(node.textContent)))) {
|
||
return { manualRequired: true, reason: '网站提示存在未作答题,请在原网页手动确认交卷' };
|
||
}
|
||
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, requiresManualSubmission: false });
|
||
}
|
||
const topic = options.getTopic(), evidence = readPaper(doc, ctx, options.getSnapshot(), topic);
|
||
const policy = attemptPolicy();
|
||
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);
|
||
const runner = options.getRunnerState();
|
||
entry.lastObservedAt = now(); entry.lastRemaining = remaining;
|
||
emit(Object.assign({}, evidence, { elapsedSeconds: Math.floor(elapsedSeconds), waitSeconds, remainingSeconds: remaining,
|
||
manualConfirmIncomplete: manualIncomplete(),
|
||
skipSingleAttemptQuiz: policy.active, singleAttemptBlocked: policy.blocked, attemptCount: policy.count,
|
||
requiresManualSubmission: manualIncomplete() && (Boolean(entry.manualRequired) || runner === 'complete' && !evidence.fullyVerified),
|
||
predictedScore: Math.round(evidence.predictedScore * 100) / 100, submitted: false, message: error || evidence.reason }));
|
||
if (policy.blocked) return blockSingleAttempt(policy);
|
||
if (!enabled) return last;
|
||
if (scope !== scopeValue(ctx, evidence, topic) || 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 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: '交卷过程中核验条件变化,等待人工检查' });
|
||
}
|
||
if (runner === 'complete') {
|
||
enabled = false; stage = 'waiting'; error = evidence.reason || '题目未全部核验,请在网页检查未核验题并手动确认交卷';
|
||
return emit({ requiresManualSubmission: true, message: error });
|
||
}
|
||
stage = 'answering'; return emit({ message: evidence.reason || '等待所有可匹配题处理并回读核验' });
|
||
}
|
||
if (manualIncomplete() && !evidence.fullyVerified) {
|
||
enabled = false; stage = 'waiting';
|
||
error = '题目未全部核验(' + evidence.verifiedCount + '/' + evidence.total + '),请在网页检查未核验题并手动确认交卷';
|
||
log(error); return emit({ requiresManualSubmission: true, message: error });
|
||
}
|
||
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.manualRequired) {
|
||
generation++; enabled = false; stage = 'waiting'; entry.stage = 'manualRequired'; entry.manualRequired = true;
|
||
error = target.reason; await persist(); log(error);
|
||
return emit({ requiresManualSubmission: true, message: error });
|
||
}
|
||
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;
|
||
const policyKeys = ['manualConfirmIncomplete', 'autoContinuePlayback', 'resumePlaybackOnReturn', 'skipSingleAttemptQuiz'];
|
||
const pendingLearningSettings = new Map();
|
||
let learningSettings = { mode: 'all', execution: 'sequential', rate: 1, answerInterval: 4, minSubmitMinutes: 6, manualConfirmIncomplete: true, autoContinuePlayback: true, resumePlaybackOnReturn: true, skipSingleAttemptQuiz: true, associations: {} };
|
||
const inScope = () => YianSiteScope.isAllowedURL(location.href);
|
||
|
||
function skipSingleAttempt() { return learning ? learning.getState().skipSingleAttemptQuiz !== false : learningSettings.skipSingleAttemptQuiz !== false; }
|
||
function attemptPolicy() {
|
||
return learning?.getAttemptPolicy() || window.YianAttemptPolicy.read(document.querySelector('app-examdetail,app-examtest,.pc_examtest'));
|
||
}
|
||
|
||
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;
|
||
const attempt = route.mode === 'test' ? attemptPolicy() : null;
|
||
const attemptBlocked = !!attempt && skipSingleAttempt() && attempt.blocked;
|
||
return { ready: inScope() && !!page && page.ready && !loading && page.key === route.key
|
||
&& page.root === route.root && page.signature === signature && !attemptBlocked,
|
||
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, skipSingleAttempt(), attempt.signature]) : '' };
|
||
}
|
||
|
||
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 || {};
|
||
const rendered = 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 });
|
||
for (const [key, pending] of pendingLearningSettings) rendered[key] = pending.value;
|
||
panel.setState({ learning: rendered });
|
||
}
|
||
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.mode === 'test' && skipSingleAttempt() && attemptPolicy().blocked) {
|
||
panel.setNotice(attemptPolicy().reason, 'warning'); refreshPanel(); return false;
|
||
}
|
||
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', ...policyKeys, '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('请选择顺序或并行');
|
||
if (policyKeys.includes(key) && typeof value !== 'boolean') 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('主题关联须使用完整标题及题库课程名称列表');
|
||
}
|
||
const pending = { value }; pendingLearningSettings.set(key, pending);
|
||
try {
|
||
if (['mode', 'execution', 'associations', 'manualConfirmIncomplete', 'skipSingleAttemptQuiz'].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);
|
||
} finally {
|
||
if (pendingLearningSettings.get(key) === pending) pendingLearningSettings.delete(key);
|
||
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') {
|
||
if (learning.getState().status !== 'running') {
|
||
panel.setNotice('当前计划已暂停,请先在任务总览点击“继续上次任务”重新核验。', 'warning'); return;
|
||
}
|
||
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: () => Object.assign({}, learningSettings, {
|
||
manualConfirmIncomplete: learning ? learning.getState().manualConfirmIncomplete !== false : learningSettings.manualConfirmIncomplete !== false,
|
||
skipSingleAttemptQuiz: skipSingleAttempt()
|
||
}), getAttemptPolicy: attemptPolicy, 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, skipSingleAttemptQuiz: quizContext.skipSingleAttemptQuiz },
|
||
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;
|
||
if (!loading) for (const key of policyKeys) if (!pendingLearningSettings.has(key) && typeof state[key] === 'boolean') learningSettings[key] = state[key];
|
||
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) {
|
||
if (route.mode === 'test' && skipSingleAttempt() && attemptPolicy().blocked && config.autoAnswer) {
|
||
config.autoAnswer = false; runKey = ''; runner.stop(attemptPolicy().reason); panel.setNotice(attemptPolicy().reason, 'warning');
|
||
}
|
||
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, { restore: true });
|
||
const restoredPolicies = learning.getState();
|
||
for (const key of policyKeys) learningSettings[key] = restoredPolicies[key];
|
||
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();
|
||
})();
|
||
|
||
})();
|