// ==UserScript==
// @name 爱问答 · 网课学习助手
// @namespace aiask
// @version 3.1.0
// @author 爱问答
// @description 全平台网课答题助手,一键解析当前页面试题并获取答案,支持作业 / 考试 / 章节测验的自动收录与答题,视频与文档等课程学习任务自动推进。已适配【超星学习通、168 网校】,更多平台持续适配中...
// @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCIgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByb2xlPSJpbWciIGFyaWEtbGFiZWw9IueIsemXruetlCI+CiAgPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iMTAiIGZpbGw9IiNDNzM5MUIiLz4KICA8cmVjdCB4PSIzLjUiIHk9IjMuNSIgd2lkdGg9IjU3IiBoZWlnaHQ9IjU3IiByeD0iNy41IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmYiIHN0cm9rZS1vcGFjaXR5PSIwLjU1IiBzdHJva2Utd2lkdGg9IjIiLz4KICA8dGV4dCB4PSIzMiIgeT0iMzMiIGZpbGw9IiNmZmYiIGZvbnQtZmFtaWx5PSJTb25ndGkgU0MsIE5vdG8gU2VyaWYgU0MsIFNpbVN1biwgc2VyaWYiIGZvbnQtc2l6ZT0iNDAiIGZvbnQtd2VpZ2h0PSI3MDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj7pl648L3RleHQ+Cjwvc3ZnPgo=
// @homepage https://www.aiask.site/
// @supportURL https://www.aiask.site/contact.html
// @match *://*.chaoxing.com/*
// @match *://xatu.168wangxiao.com/*
// @match *://os.open.com.cn/*
// @match https://www.aiask.site/import.html
// @require https://registry.npmmirror.com/vue/3.5.39/files/dist/vue.global.prod.js
// @resource chaoxingFontTable https://www.aiask.site/assets/chaoxing-font-table.json
// @connect www.aiask.site
// @connect cx.icodef.com
// @grant GM_deleteValue
// @grant GM_getResourceText
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @run-at document-start
// @antifeature payment 账号题库命中按积分计费,未命中不扣分;未登录仅第三方免费题源
// @antifeature tracking 匿名上报规则失效诊断用于远程修复,自动遮盖敏感信息
// ==/UserScript==
(function (vue) {
'use strict';
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
var _a;
var _GM_deleteValue = /* @__PURE__ */ (() => typeof GM_deleteValue != "undefined" ? GM_deleteValue : void 0)();
var _GM_getResourceText = /* @__PURE__ */ (() => typeof GM_getResourceText != "undefined" ? GM_getResourceText : void 0)();
var _GM_getValue = /* @__PURE__ */ (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
var _GM_setValue = /* @__PURE__ */ (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
var _GM_xmlhttpRequest = /* @__PURE__ */ (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)();
const DEFAULT_BACKEND_BASE_URL = "https://www.aiask.site";
const BACKEND_BASE_URL = DEFAULT_BACKEND_BASE_URL;
const IS_DEFAULT_BACKEND = BACKEND_BASE_URL === DEFAULT_BACKEND_BASE_URL;
const SCRIPT_VERSION = "3.1.0";
const DEFAULT_ROOT_PUBLIC_JWK = {
kty: "EC",
crv: "P-256",
x: "gitEZjf_WTbJYGhpmmUzKE3zUdiMsgchpxfgSdZ3WDE",
y: "n_cLcQdM4-bPQAGHvxMULiETvAu6kJl8YvIwPFGWasc"
};
function resolveRootPublicJwk() {
return DEFAULT_ROOT_PUBLIC_JWK;
}
const SECURITY_ROOT_PUBLIC_JWK = resolveRootPublicJwk();
const QuestionType = {
Single: "single",
Multiple: "multiple",
Judge: "judge",
Fill: "fill"
};
const AiAskCode = {
Ok: 0,
Unauthorized: 1,
// 未登录/凭证失效
Insufficient: 2,
// 积分不足
Busy: 3,
// 上游不可达/降级
Invalid: 4,
// 请求校验失败
RateLimited: 5
// 限流
};
var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
function getDefaultExportFromCjs(x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
}
function getAugmentedNamespace(n) {
if (n.__esModule) return n;
var f = n.default;
if (typeof f == "function") {
var a = function a2() {
if (this instanceof a2) {
return Reflect.construct(f, arguments, this.constructor);
}
return f.apply(this, arguments);
};
a.prototype = f.prototype;
} else a = {};
Object.defineProperty(a, "__esModule", { value: true });
Object.keys(n).forEach(function(k) {
var d = Object.getOwnPropertyDescriptor(n, k);
Object.defineProperty(a, k, d.get ? d : {
enumerable: true,
get: function() {
return n[k];
}
});
});
return a;
}
var md5 = { exports: {} };
const __viteBrowserExternal = {};
const __viteBrowserExternal$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
default: __viteBrowserExternal
}, Symbol.toStringTag, { value: "Module" }));
const require$$1 = /* @__PURE__ */ getAugmentedNamespace(__viteBrowserExternal$1);
/**
* [js-md5]{@link https://github.com/emn178/js-md5}
*
* @namespace md5
* @version 0.8.3
* @author Chen, Yi-Cyuan [emn178@gmail.com]
* @copyright Chen, Yi-Cyuan 2014-2023
* @license MIT
*/
(function(module) {
(function() {
var INPUT_ERROR = "input is invalid type";
var FINALIZE_ERROR = "finalize already called";
var WINDOW = typeof window === "object";
var root = WINDOW ? window : {};
if (root.JS_MD5_NO_WINDOW) {
WINDOW = false;
}
var WEB_WORKER = !WINDOW && typeof self === "object";
var NODE_JS = !root.JS_MD5_NO_NODE_JS && typeof process === "object" && process.versions && process.versions.node;
if (NODE_JS) {
root = commonjsGlobal;
} else if (WEB_WORKER) {
root = self;
}
var COMMON_JS = !root.JS_MD5_NO_COMMON_JS && true && module.exports;
var ARRAY_BUFFER = !root.JS_MD5_NO_ARRAY_BUFFER && typeof ArrayBuffer !== "undefined";
var HEX_CHARS = "0123456789abcdef".split("");
var EXTRA = [128, 32768, 8388608, -2147483648];
var SHIFT = [0, 8, 16, 24];
var OUTPUT_TYPES = ["hex", "array", "digest", "buffer", "arrayBuffer", "base64"];
var BASE64_ENCODE_CHAR = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("");
var blocks = [], buffer8;
if (ARRAY_BUFFER) {
var buffer = new ArrayBuffer(68);
buffer8 = new Uint8Array(buffer);
blocks = new Uint32Array(buffer);
}
var isArray = Array.isArray;
if (root.JS_MD5_NO_NODE_JS || !isArray) {
isArray = function(obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
};
}
var isView = ArrayBuffer.isView;
if (ARRAY_BUFFER && (root.JS_MD5_NO_ARRAY_BUFFER_IS_VIEW || !isView)) {
isView = function(obj) {
return typeof obj === "object" && obj.buffer && obj.buffer.constructor === ArrayBuffer;
};
}
var formatMessage = function(message) {
var type = typeof message;
if (type === "string") {
return [message, true];
}
if (type !== "object" || message === null) {
throw new Error(INPUT_ERROR);
}
if (ARRAY_BUFFER && message.constructor === ArrayBuffer) {
return [new Uint8Array(message), false];
}
if (!isArray(message) && !isView(message)) {
throw new Error(INPUT_ERROR);
}
return [message, false];
};
var createOutputMethod = function(outputType) {
return function(message) {
return new Md5(true).update(message)[outputType]();
};
};
var createMethod = function() {
var method = createOutputMethod("hex");
if (NODE_JS) {
method = nodeWrap(method);
}
method.create = function() {
return new Md5();
};
method.update = function(message) {
return method.create().update(message);
};
for (var i = 0; i < OUTPUT_TYPES.length; ++i) {
var type = OUTPUT_TYPES[i];
method[type] = createOutputMethod(type);
}
return method;
};
var nodeWrap = function(method) {
var crypto2 = require$$1;
var Buffer2 = require$$1.Buffer;
var bufferFrom;
if (Buffer2.from && !root.JS_MD5_NO_BUFFER_FROM) {
bufferFrom = Buffer2.from;
} else {
bufferFrom = function(message) {
return new Buffer2(message);
};
}
var nodeMethod = function(message) {
if (typeof message === "string") {
return crypto2.createHash("md5").update(message, "utf8").digest("hex");
} else {
if (message === null || message === void 0) {
throw new Error(INPUT_ERROR);
} else if (message.constructor === ArrayBuffer) {
message = new Uint8Array(message);
}
}
if (isArray(message) || isView(message) || message.constructor === Buffer2) {
return crypto2.createHash("md5").update(bufferFrom(message)).digest("hex");
} else {
return method(message);
}
};
return nodeMethod;
};
var createHmacOutputMethod = function(outputType) {
return function(key, message) {
return new HmacMd5(key, true).update(message)[outputType]();
};
};
var createHmacMethod = function() {
var method = createHmacOutputMethod("hex");
method.create = function(key) {
return new HmacMd5(key);
};
method.update = function(key, message) {
return method.create(key).update(message);
};
for (var i = 0; i < OUTPUT_TYPES.length; ++i) {
var type = OUTPUT_TYPES[i];
method[type] = createHmacOutputMethod(type);
}
return method;
};
function Md5(sharedMemory) {
if (sharedMemory) {
blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] = blocks[4] = blocks[5] = blocks[6] = blocks[7] = blocks[8] = blocks[9] = blocks[10] = blocks[11] = blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
this.blocks = blocks;
this.buffer8 = buffer8;
} else {
if (ARRAY_BUFFER) {
var buffer2 = new ArrayBuffer(68);
this.buffer8 = new Uint8Array(buffer2);
this.blocks = new Uint32Array(buffer2);
} else {
this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
}
}
this.h0 = this.h1 = this.h2 = this.h3 = this.start = this.bytes = this.hBytes = 0;
this.finalized = this.hashed = false;
this.first = true;
}
Md5.prototype.update = function(message) {
if (this.finalized) {
throw new Error(FINALIZE_ERROR);
}
var result = formatMessage(message);
message = result[0];
var isString = result[1];
var code, index = 0, i, length = message.length, blocks2 = this.blocks;
var buffer82 = this.buffer8;
while (index < length) {
if (this.hashed) {
this.hashed = false;
blocks2[0] = blocks2[16];
blocks2[16] = blocks2[1] = blocks2[2] = blocks2[3] = blocks2[4] = blocks2[5] = blocks2[6] = blocks2[7] = blocks2[8] = blocks2[9] = blocks2[10] = blocks2[11] = blocks2[12] = blocks2[13] = blocks2[14] = blocks2[15] = 0;
}
if (isString) {
if (ARRAY_BUFFER) {
for (i = this.start; index < length && i < 64; ++index) {
code = message.charCodeAt(index);
if (code < 128) {
buffer82[i++] = code;
} else if (code < 2048) {
buffer82[i++] = 192 | code >>> 6;
buffer82[i++] = 128 | code & 63;
} else if (code < 55296 || code >= 57344) {
buffer82[i++] = 224 | code >>> 12;
buffer82[i++] = 128 | code >>> 6 & 63;
buffer82[i++] = 128 | code & 63;
} else {
code = 65536 + ((code & 1023) << 10 | message.charCodeAt(++index) & 1023);
buffer82[i++] = 240 | code >>> 18;
buffer82[i++] = 128 | code >>> 12 & 63;
buffer82[i++] = 128 | code >>> 6 & 63;
buffer82[i++] = 128 | code & 63;
}
}
} else {
for (i = this.start; index < length && i < 64; ++index) {
code = message.charCodeAt(index);
if (code < 128) {
blocks2[i >>> 2] |= code << SHIFT[i++ & 3];
} else if (code < 2048) {
blocks2[i >>> 2] |= (192 | code >>> 6) << SHIFT[i++ & 3];
blocks2[i >>> 2] |= (128 | code & 63) << SHIFT[i++ & 3];
} else if (code < 55296 || code >= 57344) {
blocks2[i >>> 2] |= (224 | code >>> 12) << SHIFT[i++ & 3];
blocks2[i >>> 2] |= (128 | code >>> 6 & 63) << SHIFT[i++ & 3];
blocks2[i >>> 2] |= (128 | code & 63) << SHIFT[i++ & 3];
} else {
code = 65536 + ((code & 1023) << 10 | message.charCodeAt(++index) & 1023);
blocks2[i >>> 2] |= (240 | code >>> 18) << SHIFT[i++ & 3];
blocks2[i >>> 2] |= (128 | code >>> 12 & 63) << SHIFT[i++ & 3];
blocks2[i >>> 2] |= (128 | code >>> 6 & 63) << SHIFT[i++ & 3];
blocks2[i >>> 2] |= (128 | code & 63) << SHIFT[i++ & 3];
}
}
}
} else {
if (ARRAY_BUFFER) {
for (i = this.start; index < length && i < 64; ++index) {
buffer82[i++] = message[index];
}
} else {
for (i = this.start; index < length && i < 64; ++index) {
blocks2[i >>> 2] |= message[index] << SHIFT[i++ & 3];
}
}
}
this.lastByteIndex = i;
this.bytes += i - this.start;
if (i >= 64) {
this.start = i - 64;
this.hash();
this.hashed = true;
} else {
this.start = i;
}
}
if (this.bytes > 4294967295) {
this.hBytes += this.bytes / 4294967296 << 0;
this.bytes = this.bytes % 4294967296;
}
return this;
};
Md5.prototype.finalize = function() {
if (this.finalized) {
return;
}
this.finalized = true;
var blocks2 = this.blocks, i = this.lastByteIndex;
blocks2[i >>> 2] |= EXTRA[i & 3];
if (i >= 56) {
if (!this.hashed) {
this.hash();
}
blocks2[0] = blocks2[16];
blocks2[16] = blocks2[1] = blocks2[2] = blocks2[3] = blocks2[4] = blocks2[5] = blocks2[6] = blocks2[7] = blocks2[8] = blocks2[9] = blocks2[10] = blocks2[11] = blocks2[12] = blocks2[13] = blocks2[14] = blocks2[15] = 0;
}
blocks2[14] = this.bytes << 3;
blocks2[15] = this.hBytes << 3 | this.bytes >>> 29;
this.hash();
};
Md5.prototype.hash = function() {
var a, b, c, d, bc, da, blocks2 = this.blocks;
if (this.first) {
a = blocks2[0] - 680876937;
a = (a << 7 | a >>> 25) - 271733879 << 0;
d = (-1732584194 ^ a & 2004318071) + blocks2[1] - 117830708;
d = (d << 12 | d >>> 20) + a << 0;
c = (-271733879 ^ d & (a ^ -271733879)) + blocks2[2] - 1126478375;
c = (c << 17 | c >>> 15) + d << 0;
b = (a ^ c & (d ^ a)) + blocks2[3] - 1316259209;
b = (b << 22 | b >>> 10) + c << 0;
} else {
a = this.h0;
b = this.h1;
c = this.h2;
d = this.h3;
a += (d ^ b & (c ^ d)) + blocks2[0] - 680876936;
a = (a << 7 | a >>> 25) + b << 0;
d += (c ^ a & (b ^ c)) + blocks2[1] - 389564586;
d = (d << 12 | d >>> 20) + a << 0;
c += (b ^ d & (a ^ b)) + blocks2[2] + 606105819;
c = (c << 17 | c >>> 15) + d << 0;
b += (a ^ c & (d ^ a)) + blocks2[3] - 1044525330;
b = (b << 22 | b >>> 10) + c << 0;
}
a += (d ^ b & (c ^ d)) + blocks2[4] - 176418897;
a = (a << 7 | a >>> 25) + b << 0;
d += (c ^ a & (b ^ c)) + blocks2[5] + 1200080426;
d = (d << 12 | d >>> 20) + a << 0;
c += (b ^ d & (a ^ b)) + blocks2[6] - 1473231341;
c = (c << 17 | c >>> 15) + d << 0;
b += (a ^ c & (d ^ a)) + blocks2[7] - 45705983;
b = (b << 22 | b >>> 10) + c << 0;
a += (d ^ b & (c ^ d)) + blocks2[8] + 1770035416;
a = (a << 7 | a >>> 25) + b << 0;
d += (c ^ a & (b ^ c)) + blocks2[9] - 1958414417;
d = (d << 12 | d >>> 20) + a << 0;
c += (b ^ d & (a ^ b)) + blocks2[10] - 42063;
c = (c << 17 | c >>> 15) + d << 0;
b += (a ^ c & (d ^ a)) + blocks2[11] - 1990404162;
b = (b << 22 | b >>> 10) + c << 0;
a += (d ^ b & (c ^ d)) + blocks2[12] + 1804603682;
a = (a << 7 | a >>> 25) + b << 0;
d += (c ^ a & (b ^ c)) + blocks2[13] - 40341101;
d = (d << 12 | d >>> 20) + a << 0;
c += (b ^ d & (a ^ b)) + blocks2[14] - 1502002290;
c = (c << 17 | c >>> 15) + d << 0;
b += (a ^ c & (d ^ a)) + blocks2[15] + 1236535329;
b = (b << 22 | b >>> 10) + c << 0;
a += (c ^ d & (b ^ c)) + blocks2[1] - 165796510;
a = (a << 5 | a >>> 27) + b << 0;
d += (b ^ c & (a ^ b)) + blocks2[6] - 1069501632;
d = (d << 9 | d >>> 23) + a << 0;
c += (a ^ b & (d ^ a)) + blocks2[11] + 643717713;
c = (c << 14 | c >>> 18) + d << 0;
b += (d ^ a & (c ^ d)) + blocks2[0] - 373897302;
b = (b << 20 | b >>> 12) + c << 0;
a += (c ^ d & (b ^ c)) + blocks2[5] - 701558691;
a = (a << 5 | a >>> 27) + b << 0;
d += (b ^ c & (a ^ b)) + blocks2[10] + 38016083;
d = (d << 9 | d >>> 23) + a << 0;
c += (a ^ b & (d ^ a)) + blocks2[15] - 660478335;
c = (c << 14 | c >>> 18) + d << 0;
b += (d ^ a & (c ^ d)) + blocks2[4] - 405537848;
b = (b << 20 | b >>> 12) + c << 0;
a += (c ^ d & (b ^ c)) + blocks2[9] + 568446438;
a = (a << 5 | a >>> 27) + b << 0;
d += (b ^ c & (a ^ b)) + blocks2[14] - 1019803690;
d = (d << 9 | d >>> 23) + a << 0;
c += (a ^ b & (d ^ a)) + blocks2[3] - 187363961;
c = (c << 14 | c >>> 18) + d << 0;
b += (d ^ a & (c ^ d)) + blocks2[8] + 1163531501;
b = (b << 20 | b >>> 12) + c << 0;
a += (c ^ d & (b ^ c)) + blocks2[13] - 1444681467;
a = (a << 5 | a >>> 27) + b << 0;
d += (b ^ c & (a ^ b)) + blocks2[2] - 51403784;
d = (d << 9 | d >>> 23) + a << 0;
c += (a ^ b & (d ^ a)) + blocks2[7] + 1735328473;
c = (c << 14 | c >>> 18) + d << 0;
b += (d ^ a & (c ^ d)) + blocks2[12] - 1926607734;
b = (b << 20 | b >>> 12) + c << 0;
bc = b ^ c;
a += (bc ^ d) + blocks2[5] - 378558;
a = (a << 4 | a >>> 28) + b << 0;
d += (bc ^ a) + blocks2[8] - 2022574463;
d = (d << 11 | d >>> 21) + a << 0;
da = d ^ a;
c += (da ^ b) + blocks2[11] + 1839030562;
c = (c << 16 | c >>> 16) + d << 0;
b += (da ^ c) + blocks2[14] - 35309556;
b = (b << 23 | b >>> 9) + c << 0;
bc = b ^ c;
a += (bc ^ d) + blocks2[1] - 1530992060;
a = (a << 4 | a >>> 28) + b << 0;
d += (bc ^ a) + blocks2[4] + 1272893353;
d = (d << 11 | d >>> 21) + a << 0;
da = d ^ a;
c += (da ^ b) + blocks2[7] - 155497632;
c = (c << 16 | c >>> 16) + d << 0;
b += (da ^ c) + blocks2[10] - 1094730640;
b = (b << 23 | b >>> 9) + c << 0;
bc = b ^ c;
a += (bc ^ d) + blocks2[13] + 681279174;
a = (a << 4 | a >>> 28) + b << 0;
d += (bc ^ a) + blocks2[0] - 358537222;
d = (d << 11 | d >>> 21) + a << 0;
da = d ^ a;
c += (da ^ b) + blocks2[3] - 722521979;
c = (c << 16 | c >>> 16) + d << 0;
b += (da ^ c) + blocks2[6] + 76029189;
b = (b << 23 | b >>> 9) + c << 0;
bc = b ^ c;
a += (bc ^ d) + blocks2[9] - 640364487;
a = (a << 4 | a >>> 28) + b << 0;
d += (bc ^ a) + blocks2[12] - 421815835;
d = (d << 11 | d >>> 21) + a << 0;
da = d ^ a;
c += (da ^ b) + blocks2[15] + 530742520;
c = (c << 16 | c >>> 16) + d << 0;
b += (da ^ c) + blocks2[2] - 995338651;
b = (b << 23 | b >>> 9) + c << 0;
a += (c ^ (b | ~d)) + blocks2[0] - 198630844;
a = (a << 6 | a >>> 26) + b << 0;
d += (b ^ (a | ~c)) + blocks2[7] + 1126891415;
d = (d << 10 | d >>> 22) + a << 0;
c += (a ^ (d | ~b)) + blocks2[14] - 1416354905;
c = (c << 15 | c >>> 17) + d << 0;
b += (d ^ (c | ~a)) + blocks2[5] - 57434055;
b = (b << 21 | b >>> 11) + c << 0;
a += (c ^ (b | ~d)) + blocks2[12] + 1700485571;
a = (a << 6 | a >>> 26) + b << 0;
d += (b ^ (a | ~c)) + blocks2[3] - 1894986606;
d = (d << 10 | d >>> 22) + a << 0;
c += (a ^ (d | ~b)) + blocks2[10] - 1051523;
c = (c << 15 | c >>> 17) + d << 0;
b += (d ^ (c | ~a)) + blocks2[1] - 2054922799;
b = (b << 21 | b >>> 11) + c << 0;
a += (c ^ (b | ~d)) + blocks2[8] + 1873313359;
a = (a << 6 | a >>> 26) + b << 0;
d += (b ^ (a | ~c)) + blocks2[15] - 30611744;
d = (d << 10 | d >>> 22) + a << 0;
c += (a ^ (d | ~b)) + blocks2[6] - 1560198380;
c = (c << 15 | c >>> 17) + d << 0;
b += (d ^ (c | ~a)) + blocks2[13] + 1309151649;
b = (b << 21 | b >>> 11) + c << 0;
a += (c ^ (b | ~d)) + blocks2[4] - 145523070;
a = (a << 6 | a >>> 26) + b << 0;
d += (b ^ (a | ~c)) + blocks2[11] - 1120210379;
d = (d << 10 | d >>> 22) + a << 0;
c += (a ^ (d | ~b)) + blocks2[2] + 718787259;
c = (c << 15 | c >>> 17) + d << 0;
b += (d ^ (c | ~a)) + blocks2[9] - 343485551;
b = (b << 21 | b >>> 11) + c << 0;
if (this.first) {
this.h0 = a + 1732584193 << 0;
this.h1 = b - 271733879 << 0;
this.h2 = c - 1732584194 << 0;
this.h3 = d + 271733878 << 0;
this.first = false;
} else {
this.h0 = this.h0 + a << 0;
this.h1 = this.h1 + b << 0;
this.h2 = this.h2 + c << 0;
this.h3 = this.h3 + d << 0;
}
};
Md5.prototype.hex = function() {
this.finalize();
var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3;
return HEX_CHARS[h0 >>> 4 & 15] + HEX_CHARS[h0 & 15] + HEX_CHARS[h0 >>> 12 & 15] + HEX_CHARS[h0 >>> 8 & 15] + HEX_CHARS[h0 >>> 20 & 15] + HEX_CHARS[h0 >>> 16 & 15] + HEX_CHARS[h0 >>> 28 & 15] + HEX_CHARS[h0 >>> 24 & 15] + HEX_CHARS[h1 >>> 4 & 15] + HEX_CHARS[h1 & 15] + HEX_CHARS[h1 >>> 12 & 15] + HEX_CHARS[h1 >>> 8 & 15] + HEX_CHARS[h1 >>> 20 & 15] + HEX_CHARS[h1 >>> 16 & 15] + HEX_CHARS[h1 >>> 28 & 15] + HEX_CHARS[h1 >>> 24 & 15] + HEX_CHARS[h2 >>> 4 & 15] + HEX_CHARS[h2 & 15] + HEX_CHARS[h2 >>> 12 & 15] + HEX_CHARS[h2 >>> 8 & 15] + HEX_CHARS[h2 >>> 20 & 15] + HEX_CHARS[h2 >>> 16 & 15] + HEX_CHARS[h2 >>> 28 & 15] + HEX_CHARS[h2 >>> 24 & 15] + HEX_CHARS[h3 >>> 4 & 15] + HEX_CHARS[h3 & 15] + HEX_CHARS[h3 >>> 12 & 15] + HEX_CHARS[h3 >>> 8 & 15] + HEX_CHARS[h3 >>> 20 & 15] + HEX_CHARS[h3 >>> 16 & 15] + HEX_CHARS[h3 >>> 28 & 15] + HEX_CHARS[h3 >>> 24 & 15];
};
Md5.prototype.toString = Md5.prototype.hex;
Md5.prototype.digest = function() {
this.finalize();
var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3;
return [
h0 & 255,
h0 >>> 8 & 255,
h0 >>> 16 & 255,
h0 >>> 24 & 255,
h1 & 255,
h1 >>> 8 & 255,
h1 >>> 16 & 255,
h1 >>> 24 & 255,
h2 & 255,
h2 >>> 8 & 255,
h2 >>> 16 & 255,
h2 >>> 24 & 255,
h3 & 255,
h3 >>> 8 & 255,
h3 >>> 16 & 255,
h3 >>> 24 & 255
];
};
Md5.prototype.array = Md5.prototype.digest;
Md5.prototype.arrayBuffer = function() {
this.finalize();
var buffer2 = new ArrayBuffer(16);
var blocks2 = new Uint32Array(buffer2);
blocks2[0] = this.h0;
blocks2[1] = this.h1;
blocks2[2] = this.h2;
blocks2[3] = this.h3;
return buffer2;
};
Md5.prototype.buffer = Md5.prototype.arrayBuffer;
Md5.prototype.base64 = function() {
var v1, v2, v3, base64Str = "", bytes = this.array();
for (var i = 0; i < 15; ) {
v1 = bytes[i++];
v2 = bytes[i++];
v3 = bytes[i++];
base64Str += BASE64_ENCODE_CHAR[v1 >>> 2] + BASE64_ENCODE_CHAR[(v1 << 4 | v2 >>> 4) & 63] + BASE64_ENCODE_CHAR[(v2 << 2 | v3 >>> 6) & 63] + BASE64_ENCODE_CHAR[v3 & 63];
}
v1 = bytes[i];
base64Str += BASE64_ENCODE_CHAR[v1 >>> 2] + BASE64_ENCODE_CHAR[v1 << 4 & 63] + "==";
return base64Str;
};
function HmacMd5(key, sharedMemory) {
var i, result = formatMessage(key);
key = result[0];
if (result[1]) {
var bytes = [], length = key.length, index = 0, code;
for (i = 0; i < length; ++i) {
code = key.charCodeAt(i);
if (code < 128) {
bytes[index++] = code;
} else if (code < 2048) {
bytes[index++] = 192 | code >>> 6;
bytes[index++] = 128 | code & 63;
} else if (code < 55296 || code >= 57344) {
bytes[index++] = 224 | code >>> 12;
bytes[index++] = 128 | code >>> 6 & 63;
bytes[index++] = 128 | code & 63;
} else {
code = 65536 + ((code & 1023) << 10 | key.charCodeAt(++i) & 1023);
bytes[index++] = 240 | code >>> 18;
bytes[index++] = 128 | code >>> 12 & 63;
bytes[index++] = 128 | code >>> 6 & 63;
bytes[index++] = 128 | code & 63;
}
}
key = bytes;
}
if (key.length > 64) {
key = new Md5(true).update(key).array();
}
var oKeyPad = [], iKeyPad = [];
for (i = 0; i < 64; ++i) {
var b = key[i] || 0;
oKeyPad[i] = 92 ^ b;
iKeyPad[i] = 54 ^ b;
}
Md5.call(this, sharedMemory);
this.update(iKeyPad);
this.oKeyPad = oKeyPad;
this.inner = true;
this.sharedMemory = sharedMemory;
}
HmacMd5.prototype = new Md5();
HmacMd5.prototype.finalize = function() {
Md5.prototype.finalize.call(this);
if (this.inner) {
this.inner = false;
var innerHash = this.array();
Md5.call(this, this.sharedMemory);
this.update(this.oKeyPad);
this.update(innerHash);
Md5.prototype.finalize.call(this);
}
};
var exports = createMethod();
exports.md5 = exports;
exports.md5.hmac = createHmacMethod();
if (COMMON_JS) {
module.exports = exports;
} else {
root.md5 = exports;
}
})();
})(md5);
var md5Exports = md5.exports;
const RAW_TEXT_CLOSE_PATTERN = /<\/textarea/gi;
const TEXT_SENTINEL = "x";
const preserveCarriageReturns = (value) => value.replace(/\r/g, "
");
const decodeOnce = (value) => {
var _a2, _b;
const neutralized = preserveCarriageReturns(
value.replace(
RAW_TEXT_CLOSE_PATTERN,
(delimiter) => `<${delimiter.slice(1)}`
)
);
const parsed = new DOMParser().parseFromString(
``,
"text/html"
);
return ((_b = (_a2 = parsed.querySelector("textarea")) == null ? void 0 : _a2.textContent) == null ? void 0 : _b.slice(TEXT_SENTINEL.length)) ?? "";
};
const decodeAttributeOnce = (value) => {
var _a2;
const embedded = preserveCarriageReturns(value).replace(/"/g, """);
const parsed = new DOMParser().parseFromString(
`
`,
"text/html"
);
return ((_a2 = parsed.querySelector("div")) == null ? void 0 : _a2.getAttribute("data-value")) ?? "";
};
const decodeHTML = (value) => decodeOnce(value);
const decodeHTMLAttribute = (value) => decodeAttributeOnce(value);
const decodeAttr = (value) => decodeHTMLAttribute(value);
const decodeText = (value) => decodeHTML(value);
const IMAGE_TAG_NAME = String.raw`img(?=[\s/>])`;
const IMAGE_TAG_START_PATTERN = new RegExp(`^<${IMAGE_TAG_NAME}`, "i");
const IMAGE_TAG_PATTERN = new RegExp(`<${IMAGE_TAG_NAME}[^>]*>`, "gi");
const escapeAttr = (value) => value.replace(/&/g, "&").replace(/"/g, """).replace(//g, ">");
function serializeQuestionText(value) {
return value.replace(/&/g, "&").replace(//g, ">");
}
const isHttpImageSrc = (src) => {
if (!/^https?:\/\//i.test(src)) return false;
try {
const url = new URL(src);
return (url.protocol === "http:" || url.protocol === "https:") && Boolean(url.hostname);
} catch {
return false;
}
};
function imageSrcFromTag(tag) {
if (!IMAGE_TAG_START_PATTERN.test(tag.trim())) return "";
const match = tag.match(
/(?:^|\s)src\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i
);
return decodeAttr((match == null ? void 0 : match[1]) ?? (match == null ? void 0 : match[2]) ?? (match == null ? void 0 : match[3]) ?? "");
}
function serializeImageToken(src) {
return isHttpImageSrc(src) ? `
` : "";
}
function stripImageSrcVolatileParts(src) {
return src.replace(/[?#].*$/, "");
}
function splitQuestionContent(value) {
const source = String(value ?? "");
const out = [];
const pushText = (text) => {
if (text) out.push({ type: "text", value: text });
};
let cursor = 0;
for (const match of source.matchAll(IMAGE_TAG_PATTERN)) {
const index = match.index ?? 0;
pushText(source.slice(cursor, index));
const src = imageSrcFromTag(match[0]);
if (isHttpImageSrc(src)) out.push({ type: "image", value: src });
cursor = index + match[0].length;
}
pushText(source.slice(cursor));
return out;
}
const stripUntrustedTags = (value) => value.replace(/<\/?[A-Za-z][^>]*>/g, "");
function normalizeImageTagsForHash(value) {
return splitQuestionContent(value).map(
(part) => part.type === "image" ? serializeImageToken(stripImageSrcVolatileParts(part.value)) : decodeText(part.value)
).join("");
}
function normalizeQuestionContentForMatch(value) {
return splitQuestionContent(value).map(
(part) => part.type === "image" ? stripImageSrcVolatileParts(part.value) : decodeText(stripUntrustedTags(part.value))
).join("");
}
function questionTextForSearch(value) {
return parseQuestionContent(value).map(
(part) => part.type === "image" ? serializeImageToken(part.value) : part.value
).join("");
}
function parseQuestionContent(value, options = {}) {
const out = [];
const pushText = (text) => {
if (!text) return;
const last = out.at(-1);
if ((last == null ? void 0 : last.type) === "text") last.value += text;
else out.push({ type: "text", value: text });
};
for (const part of splitQuestionContent(value)) {
if (part.type === "image") out.push(part);
else {
const text = options.stripUntrustedTags === false ? part.value : stripUntrustedTags(part.value);
pushText(decodeText(text));
}
}
return out;
}
var sha256$1 = { exports: {} };
/**
* [js-sha256]{@link https://github.com/emn178/js-sha256}
*
* @version 0.11.1
* @author Chen, Yi-Cyuan [emn178@gmail.com]
* @copyright Chen, Yi-Cyuan 2014-2025
* @license MIT
*/
(function(module) {
(function() {
var ERROR = "input is invalid type";
var WINDOW = typeof window === "object";
var root = WINDOW ? window : {};
if (root.JS_SHA256_NO_WINDOW) {
WINDOW = false;
}
var WEB_WORKER = !WINDOW && typeof self === "object";
var NODE_JS = !root.JS_SHA256_NO_NODE_JS && typeof process === "object" && process.versions && process.versions.node && process.type != "renderer";
if (NODE_JS) {
root = commonjsGlobal;
} else if (WEB_WORKER) {
root = self;
}
var COMMON_JS = !root.JS_SHA256_NO_COMMON_JS && true && module.exports;
var ARRAY_BUFFER = !root.JS_SHA256_NO_ARRAY_BUFFER && typeof ArrayBuffer !== "undefined";
var HEX_CHARS = "0123456789abcdef".split("");
var EXTRA = [-2147483648, 8388608, 32768, 128];
var SHIFT = [24, 16, 8, 0];
var K = [
1116352408,
1899447441,
3049323471,
3921009573,
961987163,
1508970993,
2453635748,
2870763221,
3624381080,
310598401,
607225278,
1426881987,
1925078388,
2162078206,
2614888103,
3248222580,
3835390401,
4022224774,
264347078,
604807628,
770255983,
1249150122,
1555081692,
1996064986,
2554220882,
2821834349,
2952996808,
3210313671,
3336571891,
3584528711,
113926993,
338241895,
666307205,
773529912,
1294757372,
1396182291,
1695183700,
1986661051,
2177026350,
2456956037,
2730485921,
2820302411,
3259730800,
3345764771,
3516065817,
3600352804,
4094571909,
275423344,
430227734,
506948616,
659060556,
883997877,
958139571,
1322822218,
1537002063,
1747873779,
1955562222,
2024104815,
2227730452,
2361852424,
2428436474,
2756734187,
3204031479,
3329325298
];
var OUTPUT_TYPES = ["hex", "array", "digest", "arrayBuffer"];
var blocks = [];
if (root.JS_SHA256_NO_NODE_JS || !Array.isArray) {
Array.isArray = function(obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
};
}
if (ARRAY_BUFFER && (root.JS_SHA256_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView)) {
ArrayBuffer.isView = function(obj) {
return typeof obj === "object" && obj.buffer && obj.buffer.constructor === ArrayBuffer;
};
}
var createOutputMethod = function(outputType, is224) {
return function(message) {
return new Sha256(is224, true).update(message)[outputType]();
};
};
var createMethod = function(is224) {
var method = createOutputMethod("hex", is224);
if (NODE_JS) {
method = nodeWrap(method, is224);
}
method.create = function() {
return new Sha256(is224);
};
method.update = function(message) {
return method.create().update(message);
};
for (var i = 0; i < OUTPUT_TYPES.length; ++i) {
var type = OUTPUT_TYPES[i];
method[type] = createOutputMethod(type, is224);
}
return method;
};
var nodeWrap = function(method, is224) {
var crypto2 = require$$1;
var Buffer2 = require$$1.Buffer;
var algorithm = is224 ? "sha224" : "sha256";
var bufferFrom;
if (Buffer2.from && !root.JS_SHA256_NO_BUFFER_FROM) {
bufferFrom = Buffer2.from;
} else {
bufferFrom = function(message) {
return new Buffer2(message);
};
}
var nodeMethod = function(message) {
if (typeof message === "string") {
return crypto2.createHash(algorithm).update(message, "utf8").digest("hex");
} else {
if (message === null || message === void 0) {
throw new Error(ERROR);
} else if (message.constructor === ArrayBuffer) {
message = new Uint8Array(message);
}
}
if (Array.isArray(message) || ArrayBuffer.isView(message) || message.constructor === Buffer2) {
return crypto2.createHash(algorithm).update(bufferFrom(message)).digest("hex");
} else {
return method(message);
}
};
return nodeMethod;
};
var createHmacOutputMethod = function(outputType, is224) {
return function(key, message) {
return new HmacSha256(key, is224, true).update(message)[outputType]();
};
};
var createHmacMethod = function(is224) {
var method = createHmacOutputMethod("hex", is224);
method.create = function(key) {
return new HmacSha256(key, is224);
};
method.update = function(key, message) {
return method.create(key).update(message);
};
for (var i = 0; i < OUTPUT_TYPES.length; ++i) {
var type = OUTPUT_TYPES[i];
method[type] = createHmacOutputMethod(type, is224);
}
return method;
};
function Sha256(is224, sharedMemory) {
if (sharedMemory) {
blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] = blocks[4] = blocks[5] = blocks[6] = blocks[7] = blocks[8] = blocks[9] = blocks[10] = blocks[11] = blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
this.blocks = blocks;
} else {
this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
}
if (is224) {
this.h0 = 3238371032;
this.h1 = 914150663;
this.h2 = 812702999;
this.h3 = 4144912697;
this.h4 = 4290775857;
this.h5 = 1750603025;
this.h6 = 1694076839;
this.h7 = 3204075428;
} else {
this.h0 = 1779033703;
this.h1 = 3144134277;
this.h2 = 1013904242;
this.h3 = 2773480762;
this.h4 = 1359893119;
this.h5 = 2600822924;
this.h6 = 528734635;
this.h7 = 1541459225;
}
this.block = this.start = this.bytes = this.hBytes = 0;
this.finalized = this.hashed = false;
this.first = true;
this.is224 = is224;
}
Sha256.prototype.update = function(message) {
if (this.finalized) {
return;
}
var notString, type = typeof message;
if (type !== "string") {
if (type === "object") {
if (message === null) {
throw new Error(ERROR);
} else if (ARRAY_BUFFER && message.constructor === ArrayBuffer) {
message = new Uint8Array(message);
} else if (!Array.isArray(message)) {
if (!ARRAY_BUFFER || !ArrayBuffer.isView(message)) {
throw new Error(ERROR);
}
}
} else {
throw new Error(ERROR);
}
notString = true;
}
var code, index = 0, i, length = message.length, blocks2 = this.blocks;
while (index < length) {
if (this.hashed) {
this.hashed = false;
blocks2[0] = this.block;
this.block = blocks2[16] = blocks2[1] = blocks2[2] = blocks2[3] = blocks2[4] = blocks2[5] = blocks2[6] = blocks2[7] = blocks2[8] = blocks2[9] = blocks2[10] = blocks2[11] = blocks2[12] = blocks2[13] = blocks2[14] = blocks2[15] = 0;
}
if (notString) {
for (i = this.start; index < length && i < 64; ++index) {
blocks2[i >>> 2] |= message[index] << SHIFT[i++ & 3];
}
} else {
for (i = this.start; index < length && i < 64; ++index) {
code = message.charCodeAt(index);
if (code < 128) {
blocks2[i >>> 2] |= code << SHIFT[i++ & 3];
} else if (code < 2048) {
blocks2[i >>> 2] |= (192 | code >>> 6) << SHIFT[i++ & 3];
blocks2[i >>> 2] |= (128 | code & 63) << SHIFT[i++ & 3];
} else if (code < 55296 || code >= 57344) {
blocks2[i >>> 2] |= (224 | code >>> 12) << SHIFT[i++ & 3];
blocks2[i >>> 2] |= (128 | code >>> 6 & 63) << SHIFT[i++ & 3];
blocks2[i >>> 2] |= (128 | code & 63) << SHIFT[i++ & 3];
} else {
code = 65536 + ((code & 1023) << 10 | message.charCodeAt(++index) & 1023);
blocks2[i >>> 2] |= (240 | code >>> 18) << SHIFT[i++ & 3];
blocks2[i >>> 2] |= (128 | code >>> 12 & 63) << SHIFT[i++ & 3];
blocks2[i >>> 2] |= (128 | code >>> 6 & 63) << SHIFT[i++ & 3];
blocks2[i >>> 2] |= (128 | code & 63) << SHIFT[i++ & 3];
}
}
}
this.lastByteIndex = i;
this.bytes += i - this.start;
if (i >= 64) {
this.block = blocks2[16];
this.start = i - 64;
this.hash();
this.hashed = true;
} else {
this.start = i;
}
}
if (this.bytes > 4294967295) {
this.hBytes += this.bytes / 4294967296 << 0;
this.bytes = this.bytes % 4294967296;
}
return this;
};
Sha256.prototype.finalize = function() {
if (this.finalized) {
return;
}
this.finalized = true;
var blocks2 = this.blocks, i = this.lastByteIndex;
blocks2[16] = this.block;
blocks2[i >>> 2] |= EXTRA[i & 3];
this.block = blocks2[16];
if (i >= 56) {
if (!this.hashed) {
this.hash();
}
blocks2[0] = this.block;
blocks2[16] = blocks2[1] = blocks2[2] = blocks2[3] = blocks2[4] = blocks2[5] = blocks2[6] = blocks2[7] = blocks2[8] = blocks2[9] = blocks2[10] = blocks2[11] = blocks2[12] = blocks2[13] = blocks2[14] = blocks2[15] = 0;
}
blocks2[14] = this.hBytes << 3 | this.bytes >>> 29;
blocks2[15] = this.bytes << 3;
this.hash();
};
Sha256.prototype.hash = function() {
var a = this.h0, b = this.h1, c = this.h2, d = this.h3, e = this.h4, f = this.h5, g = this.h6, h = this.h7, blocks2 = this.blocks, j, s0, s1, maj, t1, t2, ch, ab, da, cd, bc;
for (j = 16; j < 64; ++j) {
t1 = blocks2[j - 15];
s0 = (t1 >>> 7 | t1 << 25) ^ (t1 >>> 18 | t1 << 14) ^ t1 >>> 3;
t1 = blocks2[j - 2];
s1 = (t1 >>> 17 | t1 << 15) ^ (t1 >>> 19 | t1 << 13) ^ t1 >>> 10;
blocks2[j] = blocks2[j - 16] + s0 + blocks2[j - 7] + s1 << 0;
}
bc = b & c;
for (j = 0; j < 64; j += 4) {
if (this.first) {
if (this.is224) {
ab = 300032;
t1 = blocks2[0] - 1413257819;
h = t1 - 150054599 << 0;
d = t1 + 24177077 << 0;
} else {
ab = 704751109;
t1 = blocks2[0] - 210244248;
h = t1 - 1521486534 << 0;
d = t1 + 143694565 << 0;
}
this.first = false;
} else {
s0 = (a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10);
s1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7);
ab = a & b;
maj = ab ^ a & c ^ bc;
ch = e & f ^ ~e & g;
t1 = h + s1 + ch + K[j] + blocks2[j];
t2 = s0 + maj;
h = d + t1 << 0;
d = t1 + t2 << 0;
}
s0 = (d >>> 2 | d << 30) ^ (d >>> 13 | d << 19) ^ (d >>> 22 | d << 10);
s1 = (h >>> 6 | h << 26) ^ (h >>> 11 | h << 21) ^ (h >>> 25 | h << 7);
da = d & a;
maj = da ^ d & b ^ ab;
ch = h & e ^ ~h & f;
t1 = g + s1 + ch + K[j + 1] + blocks2[j + 1];
t2 = s0 + maj;
g = c + t1 << 0;
c = t1 + t2 << 0;
s0 = (c >>> 2 | c << 30) ^ (c >>> 13 | c << 19) ^ (c >>> 22 | c << 10);
s1 = (g >>> 6 | g << 26) ^ (g >>> 11 | g << 21) ^ (g >>> 25 | g << 7);
cd = c & d;
maj = cd ^ c & a ^ da;
ch = g & h ^ ~g & e;
t1 = f + s1 + ch + K[j + 2] + blocks2[j + 2];
t2 = s0 + maj;
f = b + t1 << 0;
b = t1 + t2 << 0;
s0 = (b >>> 2 | b << 30) ^ (b >>> 13 | b << 19) ^ (b >>> 22 | b << 10);
s1 = (f >>> 6 | f << 26) ^ (f >>> 11 | f << 21) ^ (f >>> 25 | f << 7);
bc = b & c;
maj = bc ^ b & d ^ cd;
ch = f & g ^ ~f & h;
t1 = e + s1 + ch + K[j + 3] + blocks2[j + 3];
t2 = s0 + maj;
e = a + t1 << 0;
a = t1 + t2 << 0;
this.chromeBugWorkAround = true;
}
this.h0 = this.h0 + a << 0;
this.h1 = this.h1 + b << 0;
this.h2 = this.h2 + c << 0;
this.h3 = this.h3 + d << 0;
this.h4 = this.h4 + e << 0;
this.h5 = this.h5 + f << 0;
this.h6 = this.h6 + g << 0;
this.h7 = this.h7 + h << 0;
};
Sha256.prototype.hex = function() {
this.finalize();
var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5, h6 = this.h6, h7 = this.h7;
var hex = HEX_CHARS[h0 >>> 28 & 15] + HEX_CHARS[h0 >>> 24 & 15] + HEX_CHARS[h0 >>> 20 & 15] + HEX_CHARS[h0 >>> 16 & 15] + HEX_CHARS[h0 >>> 12 & 15] + HEX_CHARS[h0 >>> 8 & 15] + HEX_CHARS[h0 >>> 4 & 15] + HEX_CHARS[h0 & 15] + HEX_CHARS[h1 >>> 28 & 15] + HEX_CHARS[h1 >>> 24 & 15] + HEX_CHARS[h1 >>> 20 & 15] + HEX_CHARS[h1 >>> 16 & 15] + HEX_CHARS[h1 >>> 12 & 15] + HEX_CHARS[h1 >>> 8 & 15] + HEX_CHARS[h1 >>> 4 & 15] + HEX_CHARS[h1 & 15] + HEX_CHARS[h2 >>> 28 & 15] + HEX_CHARS[h2 >>> 24 & 15] + HEX_CHARS[h2 >>> 20 & 15] + HEX_CHARS[h2 >>> 16 & 15] + HEX_CHARS[h2 >>> 12 & 15] + HEX_CHARS[h2 >>> 8 & 15] + HEX_CHARS[h2 >>> 4 & 15] + HEX_CHARS[h2 & 15] + HEX_CHARS[h3 >>> 28 & 15] + HEX_CHARS[h3 >>> 24 & 15] + HEX_CHARS[h3 >>> 20 & 15] + HEX_CHARS[h3 >>> 16 & 15] + HEX_CHARS[h3 >>> 12 & 15] + HEX_CHARS[h3 >>> 8 & 15] + HEX_CHARS[h3 >>> 4 & 15] + HEX_CHARS[h3 & 15] + HEX_CHARS[h4 >>> 28 & 15] + HEX_CHARS[h4 >>> 24 & 15] + HEX_CHARS[h4 >>> 20 & 15] + HEX_CHARS[h4 >>> 16 & 15] + HEX_CHARS[h4 >>> 12 & 15] + HEX_CHARS[h4 >>> 8 & 15] + HEX_CHARS[h4 >>> 4 & 15] + HEX_CHARS[h4 & 15] + HEX_CHARS[h5 >>> 28 & 15] + HEX_CHARS[h5 >>> 24 & 15] + HEX_CHARS[h5 >>> 20 & 15] + HEX_CHARS[h5 >>> 16 & 15] + HEX_CHARS[h5 >>> 12 & 15] + HEX_CHARS[h5 >>> 8 & 15] + HEX_CHARS[h5 >>> 4 & 15] + HEX_CHARS[h5 & 15] + HEX_CHARS[h6 >>> 28 & 15] + HEX_CHARS[h6 >>> 24 & 15] + HEX_CHARS[h6 >>> 20 & 15] + HEX_CHARS[h6 >>> 16 & 15] + HEX_CHARS[h6 >>> 12 & 15] + HEX_CHARS[h6 >>> 8 & 15] + HEX_CHARS[h6 >>> 4 & 15] + HEX_CHARS[h6 & 15];
if (!this.is224) {
hex += HEX_CHARS[h7 >>> 28 & 15] + HEX_CHARS[h7 >>> 24 & 15] + HEX_CHARS[h7 >>> 20 & 15] + HEX_CHARS[h7 >>> 16 & 15] + HEX_CHARS[h7 >>> 12 & 15] + HEX_CHARS[h7 >>> 8 & 15] + HEX_CHARS[h7 >>> 4 & 15] + HEX_CHARS[h7 & 15];
}
return hex;
};
Sha256.prototype.toString = Sha256.prototype.hex;
Sha256.prototype.digest = function() {
this.finalize();
var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5, h6 = this.h6, h7 = this.h7;
var arr = [
h0 >>> 24 & 255,
h0 >>> 16 & 255,
h0 >>> 8 & 255,
h0 & 255,
h1 >>> 24 & 255,
h1 >>> 16 & 255,
h1 >>> 8 & 255,
h1 & 255,
h2 >>> 24 & 255,
h2 >>> 16 & 255,
h2 >>> 8 & 255,
h2 & 255,
h3 >>> 24 & 255,
h3 >>> 16 & 255,
h3 >>> 8 & 255,
h3 & 255,
h4 >>> 24 & 255,
h4 >>> 16 & 255,
h4 >>> 8 & 255,
h4 & 255,
h5 >>> 24 & 255,
h5 >>> 16 & 255,
h5 >>> 8 & 255,
h5 & 255,
h6 >>> 24 & 255,
h6 >>> 16 & 255,
h6 >>> 8 & 255,
h6 & 255
];
if (!this.is224) {
arr.push(h7 >>> 24 & 255, h7 >>> 16 & 255, h7 >>> 8 & 255, h7 & 255);
}
return arr;
};
Sha256.prototype.array = Sha256.prototype.digest;
Sha256.prototype.arrayBuffer = function() {
this.finalize();
var buffer = new ArrayBuffer(this.is224 ? 28 : 32);
var dataView = new DataView(buffer);
dataView.setUint32(0, this.h0);
dataView.setUint32(4, this.h1);
dataView.setUint32(8, this.h2);
dataView.setUint32(12, this.h3);
dataView.setUint32(16, this.h4);
dataView.setUint32(20, this.h5);
dataView.setUint32(24, this.h6);
if (!this.is224) {
dataView.setUint32(28, this.h7);
}
return buffer;
};
function HmacSha256(key, is224, sharedMemory) {
var i, type = typeof key;
if (type === "string") {
var bytes = [], length = key.length, index = 0, code;
for (i = 0; i < length; ++i) {
code = key.charCodeAt(i);
if (code < 128) {
bytes[index++] = code;
} else if (code < 2048) {
bytes[index++] = 192 | code >>> 6;
bytes[index++] = 128 | code & 63;
} else if (code < 55296 || code >= 57344) {
bytes[index++] = 224 | code >>> 12;
bytes[index++] = 128 | code >>> 6 & 63;
bytes[index++] = 128 | code & 63;
} else {
code = 65536 + ((code & 1023) << 10 | key.charCodeAt(++i) & 1023);
bytes[index++] = 240 | code >>> 18;
bytes[index++] = 128 | code >>> 12 & 63;
bytes[index++] = 128 | code >>> 6 & 63;
bytes[index++] = 128 | code & 63;
}
}
key = bytes;
} else {
if (type === "object") {
if (key === null) {
throw new Error(ERROR);
} else if (ARRAY_BUFFER && key.constructor === ArrayBuffer) {
key = new Uint8Array(key);
} else if (!Array.isArray(key)) {
if (!ARRAY_BUFFER || !ArrayBuffer.isView(key)) {
throw new Error(ERROR);
}
}
} else {
throw new Error(ERROR);
}
}
if (key.length > 64) {
key = new Sha256(is224, true).update(key).array();
}
var oKeyPad = [], iKeyPad = [];
for (i = 0; i < 64; ++i) {
var b = key[i] || 0;
oKeyPad[i] = 92 ^ b;
iKeyPad[i] = 54 ^ b;
}
Sha256.call(this, is224, sharedMemory);
this.update(iKeyPad);
this.oKeyPad = oKeyPad;
this.inner = true;
this.sharedMemory = sharedMemory;
}
HmacSha256.prototype = new Sha256();
HmacSha256.prototype.finalize = function() {
Sha256.prototype.finalize.call(this);
if (this.inner) {
this.inner = false;
var innerHash = this.array();
Sha256.call(this, this.is224, this.sharedMemory);
this.update(this.oKeyPad);
this.update(innerHash);
Sha256.prototype.finalize.call(this);
}
};
var exports = createMethod();
exports.sha256 = exports;
exports.sha224 = createMethod(true);
exports.sha256.hmac = createHmacMethod();
exports.sha224.hmac = createHmacMethod(true);
if (COMMON_JS) {
module.exports = exports;
} else {
root.sha256 = exports.sha256;
root.sha224 = exports.sha224;
}
})();
})(sha256$1);
var sha256Exports = sha256$1.exports;
function normalizeQuestionContentForHash(value) {
return normalizeImageTagsForHash(value).replace(/\s+/g, " ").trim();
}
const sortedContent = (values) => values.map((item) => normalizeQuestionContentForHash(item.content)).sort();
const canonicalSlot = (slot) => [
normalizeQuestionContentForHash(slot.label ?? ""),
sortedContent(slot.options ?? [])
];
function semanticNode(node) {
switch (node.kind) {
case "leaf":
return [
"leaf",
node.type,
normalizeQuestionContentForHash(node.stem),
sortedContent(node.options),
node.slots.map(canonicalSlot)
];
case "composite":
return [
"composite",
node.type,
normalizeQuestionContentForHash(node.stem),
node.children.map(questionNodeHash)
];
case "matching":
return [
"matching",
node.cardinality,
normalizeQuestionContentForHash(node.stem),
sortedContent(node.left),
sortedContent(node.right)
];
}
}
function canonicalQuestionNode(node) {
return JSON.stringify(semanticNode(node));
}
function questionNodeHash(node) {
return sha256Exports.sha256(canonicalQuestionNode(node));
}
function semanticContentHash(value) {
return sha256Exports.sha256(normalizeQuestionContentForHash(value));
}
function searchUnitHash(input) {
const segments = input.stemSegments.map(normalizeQuestionContentForHash).filter(Boolean).map((value) => [new TextEncoder().encode(value).length, value]);
return sha256Exports.sha256(
JSON.stringify([
"search-unit-v2",
input.queryType,
segments,
sortedContent(input.options),
input.answerShape,
input.sourceNodeHash
])
);
}
var util;
(function(util2) {
util2.assertEqual = (_) => {
};
function assertIs(_arg) {
}
util2.assertIs = assertIs;
function assertNever(_x) {
throw new Error();
}
util2.assertNever = assertNever;
util2.arrayToEnum = (items) => {
const obj = {};
for (const item of items) {
obj[item] = item;
}
return obj;
};
util2.getValidEnumValues = (obj) => {
const validKeys = util2.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number");
const filtered = {};
for (const k of validKeys) {
filtered[k] = obj[k];
}
return util2.objectValues(filtered);
};
util2.objectValues = (obj) => {
return util2.objectKeys(obj).map(function(e) {
return obj[e];
});
};
util2.objectKeys = typeof Object.keys === "function" ? (obj) => Object.keys(obj) : (object) => {
const keys = [];
for (const key in object) {
if (Object.prototype.hasOwnProperty.call(object, key)) {
keys.push(key);
}
}
return keys;
};
util2.find = (arr, checker) => {
for (const item of arr) {
if (checker(item))
return item;
}
return void 0;
};
util2.isInteger = typeof Number.isInteger === "function" ? (val) => Number.isInteger(val) : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val;
function joinValues(array, separator = " | ") {
return array.map((val) => typeof val === "string" ? `'${val}'` : val).join(separator);
}
util2.joinValues = joinValues;
util2.jsonStringifyReplacer = (_, value) => {
if (typeof value === "bigint") {
return value.toString();
}
return value;
};
})(util || (util = {}));
var objectUtil;
(function(objectUtil2) {
objectUtil2.mergeShapes = (first, second) => {
return {
...first,
...second
// second overwrites first
};
};
})(objectUtil || (objectUtil = {}));
const ZodParsedType = util.arrayToEnum([
"string",
"nan",
"number",
"integer",
"float",
"boolean",
"date",
"bigint",
"symbol",
"function",
"undefined",
"null",
"array",
"object",
"unknown",
"promise",
"void",
"never",
"map",
"set"
]);
const getParsedType = (data) => {
const t = typeof data;
switch (t) {
case "undefined":
return ZodParsedType.undefined;
case "string":
return ZodParsedType.string;
case "number":
return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number;
case "boolean":
return ZodParsedType.boolean;
case "function":
return ZodParsedType.function;
case "bigint":
return ZodParsedType.bigint;
case "symbol":
return ZodParsedType.symbol;
case "object":
if (Array.isArray(data)) {
return ZodParsedType.array;
}
if (data === null) {
return ZodParsedType.null;
}
if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") {
return ZodParsedType.promise;
}
if (typeof Map !== "undefined" && data instanceof Map) {
return ZodParsedType.map;
}
if (typeof Set !== "undefined" && data instanceof Set) {
return ZodParsedType.set;
}
if (typeof Date !== "undefined" && data instanceof Date) {
return ZodParsedType.date;
}
return ZodParsedType.object;
default:
return ZodParsedType.unknown;
}
};
const ZodIssueCode = util.arrayToEnum([
"invalid_type",
"invalid_literal",
"custom",
"invalid_union",
"invalid_union_discriminator",
"invalid_enum_value",
"unrecognized_keys",
"invalid_arguments",
"invalid_return_type",
"invalid_date",
"invalid_string",
"too_small",
"too_big",
"invalid_intersection_types",
"not_multiple_of",
"not_finite"
]);
class ZodError extends Error {
get errors() {
return this.issues;
}
constructor(issues) {
super();
this.issues = [];
this.addIssue = (sub) => {
this.issues = [...this.issues, sub];
};
this.addIssues = (subs = []) => {
this.issues = [...this.issues, ...subs];
};
const actualProto = new.target.prototype;
if (Object.setPrototypeOf) {
Object.setPrototypeOf(this, actualProto);
} else {
this.__proto__ = actualProto;
}
this.name = "ZodError";
this.issues = issues;
}
format(_mapper) {
const mapper = _mapper || function(issue) {
return issue.message;
};
const fieldErrors = { _errors: [] };
const processError = (error) => {
for (const issue of error.issues) {
if (issue.code === "invalid_union") {
issue.unionErrors.map(processError);
} else if (issue.code === "invalid_return_type") {
processError(issue.returnTypeError);
} else if (issue.code === "invalid_arguments") {
processError(issue.argumentsError);
} else if (issue.path.length === 0) {
fieldErrors._errors.push(mapper(issue));
} else {
let curr = fieldErrors;
let i = 0;
while (i < issue.path.length) {
const el = issue.path[i];
const terminal = i === issue.path.length - 1;
if (!terminal) {
curr[el] = curr[el] || { _errors: [] };
} else {
curr[el] = curr[el] || { _errors: [] };
curr[el]._errors.push(mapper(issue));
}
curr = curr[el];
i++;
}
}
}
};
processError(this);
return fieldErrors;
}
static assert(value) {
if (!(value instanceof ZodError)) {
throw new Error(`Not a ZodError: ${value}`);
}
}
toString() {
return this.message;
}
get message() {
return JSON.stringify(this.issues, util.jsonStringifyReplacer, 2);
}
get isEmpty() {
return this.issues.length === 0;
}
flatten(mapper = (issue) => issue.message) {
const fieldErrors = {};
const formErrors = [];
for (const sub of this.issues) {
if (sub.path.length > 0) {
const firstEl = sub.path[0];
fieldErrors[firstEl] = fieldErrors[firstEl] || [];
fieldErrors[firstEl].push(mapper(sub));
} else {
formErrors.push(mapper(sub));
}
}
return { formErrors, fieldErrors };
}
get formErrors() {
return this.flatten();
}
}
ZodError.create = (issues) => {
const error = new ZodError(issues);
return error;
};
const errorMap = (issue, _ctx) => {
let message;
switch (issue.code) {
case ZodIssueCode.invalid_type:
if (issue.received === ZodParsedType.undefined) {
message = "Required";
} else {
message = `Expected ${issue.expected}, received ${issue.received}`;
}
break;
case ZodIssueCode.invalid_literal:
message = `Invalid literal value, expected ${JSON.stringify(issue.expected, util.jsonStringifyReplacer)}`;
break;
case ZodIssueCode.unrecognized_keys:
message = `Unrecognized key(s) in object: ${util.joinValues(issue.keys, ", ")}`;
break;
case ZodIssueCode.invalid_union:
message = `Invalid input`;
break;
case ZodIssueCode.invalid_union_discriminator:
message = `Invalid discriminator value. Expected ${util.joinValues(issue.options)}`;
break;
case ZodIssueCode.invalid_enum_value:
message = `Invalid enum value. Expected ${util.joinValues(issue.options)}, received '${issue.received}'`;
break;
case ZodIssueCode.invalid_arguments:
message = `Invalid function arguments`;
break;
case ZodIssueCode.invalid_return_type:
message = `Invalid function return type`;
break;
case ZodIssueCode.invalid_date:
message = `Invalid date`;
break;
case ZodIssueCode.invalid_string:
if (typeof issue.validation === "object") {
if ("includes" in issue.validation) {
message = `Invalid input: must include "${issue.validation.includes}"`;
if (typeof issue.validation.position === "number") {
message = `${message} at one or more positions greater than or equal to ${issue.validation.position}`;
}
} else if ("startsWith" in issue.validation) {
message = `Invalid input: must start with "${issue.validation.startsWith}"`;
} else if ("endsWith" in issue.validation) {
message = `Invalid input: must end with "${issue.validation.endsWith}"`;
} else {
util.assertNever(issue.validation);
}
} else if (issue.validation !== "regex") {
message = `Invalid ${issue.validation}`;
} else {
message = "Invalid";
}
break;
case ZodIssueCode.too_small:
if (issue.type === "array")
message = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`;
else if (issue.type === "string")
message = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`;
else if (issue.type === "number")
message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`;
else if (issue.type === "bigint")
message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`;
else if (issue.type === "date")
message = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`;
else
message = "Invalid input";
break;
case ZodIssueCode.too_big:
if (issue.type === "array")
message = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`;
else if (issue.type === "string")
message = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`;
else if (issue.type === "number")
message = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`;
else if (issue.type === "bigint")
message = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`;
else if (issue.type === "date")
message = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`;
else
message = "Invalid input";
break;
case ZodIssueCode.custom:
message = `Invalid input`;
break;
case ZodIssueCode.invalid_intersection_types:
message = `Intersection results could not be merged`;
break;
case ZodIssueCode.not_multiple_of:
message = `Number must be a multiple of ${issue.multipleOf}`;
break;
case ZodIssueCode.not_finite:
message = "Number must be finite";
break;
default:
message = _ctx.defaultError;
util.assertNever(issue);
}
return { message };
};
let overrideErrorMap = errorMap;
function getErrorMap() {
return overrideErrorMap;
}
const makeIssue = (params) => {
const { data, path, errorMaps, issueData } = params;
const fullPath = [...path, ...issueData.path || []];
const fullIssue = {
...issueData,
path: fullPath
};
if (issueData.message !== void 0) {
return {
...issueData,
path: fullPath,
message: issueData.message
};
}
let errorMessage = "";
const maps = errorMaps.filter((m) => !!m).slice().reverse();
for (const map of maps) {
errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message;
}
return {
...issueData,
path: fullPath,
message: errorMessage
};
};
function addIssueToContext(ctx, issueData) {
const overrideMap = getErrorMap();
const issue = makeIssue({
issueData,
data: ctx.data,
path: ctx.path,
errorMaps: [
ctx.common.contextualErrorMap,
// contextual error map is first priority
ctx.schemaErrorMap,
// then schema-bound map if available
overrideMap,
// then global override map
overrideMap === errorMap ? void 0 : errorMap
// then global default map
].filter((x) => !!x)
});
ctx.common.issues.push(issue);
}
class ParseStatus {
constructor() {
this.value = "valid";
}
dirty() {
if (this.value === "valid")
this.value = "dirty";
}
abort() {
if (this.value !== "aborted")
this.value = "aborted";
}
static mergeArray(status, results) {
const arrayValue = [];
for (const s of results) {
if (s.status === "aborted")
return INVALID;
if (s.status === "dirty")
status.dirty();
arrayValue.push(s.value);
}
return { status: status.value, value: arrayValue };
}
static async mergeObjectAsync(status, pairs) {
const syncPairs = [];
for (const pair of pairs) {
const key = await pair.key;
const value = await pair.value;
syncPairs.push({
key,
value
});
}
return ParseStatus.mergeObjectSync(status, syncPairs);
}
static mergeObjectSync(status, pairs) {
const finalObject = {};
for (const pair of pairs) {
const { key, value } = pair;
if (key.status === "aborted")
return INVALID;
if (value.status === "aborted")
return INVALID;
if (key.status === "dirty")
status.dirty();
if (value.status === "dirty")
status.dirty();
if (key.value !== "__proto__" && (typeof value.value !== "undefined" || pair.alwaysSet)) {
finalObject[key.value] = value.value;
}
}
return { status: status.value, value: finalObject };
}
}
const INVALID = Object.freeze({
status: "aborted"
});
const DIRTY = (value) => ({ status: "dirty", value });
const OK = (value) => ({ status: "valid", value });
const isAborted = (x) => x.status === "aborted";
const isDirty = (x) => x.status === "dirty";
const isValid = (x) => x.status === "valid";
const isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
var errorUtil;
(function(errorUtil2) {
errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {};
errorUtil2.toString = (message) => typeof message === "string" ? message : message == null ? void 0 : message.message;
})(errorUtil || (errorUtil = {}));
class ParseInputLazyPath {
constructor(parent, value, path, key) {
this._cachedPath = [];
this.parent = parent;
this.data = value;
this._path = path;
this._key = key;
}
get path() {
if (!this._cachedPath.length) {
if (Array.isArray(this._key)) {
this._cachedPath.push(...this._path, ...this._key);
} else {
this._cachedPath.push(...this._path, this._key);
}
}
return this._cachedPath;
}
}
const handleResult = (ctx, result) => {
if (isValid(result)) {
return { success: true, data: result.value };
} else {
if (!ctx.common.issues.length) {
throw new Error("Validation failed but no issues detected.");
}
return {
success: false,
get error() {
if (this._error)
return this._error;
const error = new ZodError(ctx.common.issues);
this._error = error;
return this._error;
}
};
}
};
function processCreateParams(params) {
if (!params)
return {};
const { errorMap: errorMap2, invalid_type_error, required_error, description } = params;
if (errorMap2 && (invalid_type_error || required_error)) {
throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`);
}
if (errorMap2)
return { errorMap: errorMap2, description };
const customMap = (iss, ctx) => {
const { message } = params;
if (iss.code === "invalid_enum_value") {
return { message: message ?? ctx.defaultError };
}
if (typeof ctx.data === "undefined") {
return { message: message ?? required_error ?? ctx.defaultError };
}
if (iss.code !== "invalid_type")
return { message: ctx.defaultError };
return { message: message ?? invalid_type_error ?? ctx.defaultError };
};
return { errorMap: customMap, description };
}
class ZodType {
get description() {
return this._def.description;
}
_getType(input) {
return getParsedType(input.data);
}
_getOrReturnCtx(input, ctx) {
return ctx || {
common: input.parent.common,
data: input.data,
parsedType: getParsedType(input.data),
schemaErrorMap: this._def.errorMap,
path: input.path,
parent: input.parent
};
}
_processInputParams(input) {
return {
status: new ParseStatus(),
ctx: {
common: input.parent.common,
data: input.data,
parsedType: getParsedType(input.data),
schemaErrorMap: this._def.errorMap,
path: input.path,
parent: input.parent
}
};
}
_parseSync(input) {
const result = this._parse(input);
if (isAsync(result)) {
throw new Error("Synchronous parse encountered promise.");
}
return result;
}
_parseAsync(input) {
const result = this._parse(input);
return Promise.resolve(result);
}
parse(data, params) {
const result = this.safeParse(data, params);
if (result.success)
return result.data;
throw result.error;
}
safeParse(data, params) {
const ctx = {
common: {
issues: [],
async: (params == null ? void 0 : params.async) ?? false,
contextualErrorMap: params == null ? void 0 : params.errorMap
},
path: (params == null ? void 0 : params.path) || [],
schemaErrorMap: this._def.errorMap,
parent: null,
data,
parsedType: getParsedType(data)
};
const result = this._parseSync({ data, path: ctx.path, parent: ctx });
return handleResult(ctx, result);
}
"~validate"(data) {
var _a2, _b;
const ctx = {
common: {
issues: [],
async: !!this["~standard"].async
},
path: [],
schemaErrorMap: this._def.errorMap,
parent: null,
data,
parsedType: getParsedType(data)
};
if (!this["~standard"].async) {
try {
const result = this._parseSync({ data, path: [], parent: ctx });
return isValid(result) ? {
value: result.value
} : {
issues: ctx.common.issues
};
} catch (err) {
if ((_b = (_a2 = err == null ? void 0 : err.message) == null ? void 0 : _a2.toLowerCase()) == null ? void 0 : _b.includes("encountered")) {
this["~standard"].async = true;
}
ctx.common = {
issues: [],
async: true
};
}
}
return this._parseAsync({ data, path: [], parent: ctx }).then((result) => isValid(result) ? {
value: result.value
} : {
issues: ctx.common.issues
});
}
async parseAsync(data, params) {
const result = await this.safeParseAsync(data, params);
if (result.success)
return result.data;
throw result.error;
}
async safeParseAsync(data, params) {
const ctx = {
common: {
issues: [],
contextualErrorMap: params == null ? void 0 : params.errorMap,
async: true
},
path: (params == null ? void 0 : params.path) || [],
schemaErrorMap: this._def.errorMap,
parent: null,
data,
parsedType: getParsedType(data)
};
const maybeAsyncResult = this._parse({ data, path: ctx.path, parent: ctx });
const result = await (isAsync(maybeAsyncResult) ? maybeAsyncResult : Promise.resolve(maybeAsyncResult));
return handleResult(ctx, result);
}
refine(check, message) {
const getIssueProperties = (val) => {
if (typeof message === "string" || typeof message === "undefined") {
return { message };
} else if (typeof message === "function") {
return message(val);
} else {
return message;
}
};
return this._refinement((val, ctx) => {
const result = check(val);
const setError = () => ctx.addIssue({
code: ZodIssueCode.custom,
...getIssueProperties(val)
});
if (typeof Promise !== "undefined" && result instanceof Promise) {
return result.then((data) => {
if (!data) {
setError();
return false;
} else {
return true;
}
});
}
if (!result) {
setError();
return false;
} else {
return true;
}
});
}
refinement(check, refinementData) {
return this._refinement((val, ctx) => {
if (!check(val)) {
ctx.addIssue(typeof refinementData === "function" ? refinementData(val, ctx) : refinementData);
return false;
} else {
return true;
}
});
}
_refinement(refinement) {
return new ZodEffects({
schema: this,
typeName: ZodFirstPartyTypeKind.ZodEffects,
effect: { type: "refinement", refinement }
});
}
superRefine(refinement) {
return this._refinement(refinement);
}
constructor(def) {
this.spa = this.safeParseAsync;
this._def = def;
this.parse = this.parse.bind(this);
this.safeParse = this.safeParse.bind(this);
this.parseAsync = this.parseAsync.bind(this);
this.safeParseAsync = this.safeParseAsync.bind(this);
this.spa = this.spa.bind(this);
this.refine = this.refine.bind(this);
this.refinement = this.refinement.bind(this);
this.superRefine = this.superRefine.bind(this);
this.optional = this.optional.bind(this);
this.nullable = this.nullable.bind(this);
this.nullish = this.nullish.bind(this);
this.array = this.array.bind(this);
this.promise = this.promise.bind(this);
this.or = this.or.bind(this);
this.and = this.and.bind(this);
this.transform = this.transform.bind(this);
this.brand = this.brand.bind(this);
this.default = this.default.bind(this);
this.catch = this.catch.bind(this);
this.describe = this.describe.bind(this);
this.pipe = this.pipe.bind(this);
this.readonly = this.readonly.bind(this);
this.isNullable = this.isNullable.bind(this);
this.isOptional = this.isOptional.bind(this);
this["~standard"] = {
version: 1,
vendor: "zod",
validate: (data) => this["~validate"](data)
};
}
optional() {
return ZodOptional.create(this, this._def);
}
nullable() {
return ZodNullable.create(this, this._def);
}
nullish() {
return this.nullable().optional();
}
array() {
return ZodArray.create(this);
}
promise() {
return ZodPromise.create(this, this._def);
}
or(option) {
return ZodUnion.create([this, option], this._def);
}
and(incoming) {
return ZodIntersection.create(this, incoming, this._def);
}
transform(transform) {
return new ZodEffects({
...processCreateParams(this._def),
schema: this,
typeName: ZodFirstPartyTypeKind.ZodEffects,
effect: { type: "transform", transform }
});
}
default(def) {
const defaultValueFunc = typeof def === "function" ? def : () => def;
return new ZodDefault({
...processCreateParams(this._def),
innerType: this,
defaultValue: defaultValueFunc,
typeName: ZodFirstPartyTypeKind.ZodDefault
});
}
brand() {
return new ZodBranded({
typeName: ZodFirstPartyTypeKind.ZodBranded,
type: this,
...processCreateParams(this._def)
});
}
catch(def) {
const catchValueFunc = typeof def === "function" ? def : () => def;
return new ZodCatch({
...processCreateParams(this._def),
innerType: this,
catchValue: catchValueFunc,
typeName: ZodFirstPartyTypeKind.ZodCatch
});
}
describe(description) {
const This = this.constructor;
return new This({
...this._def,
description
});
}
pipe(target) {
return ZodPipeline.create(this, target);
}
readonly() {
return ZodReadonly.create(this);
}
isOptional() {
return this.safeParse(void 0).success;
}
isNullable() {
return this.safeParse(null).success;
}
}
const cuidRegex = /^c[^\s-]{8,}$/i;
const cuid2Regex = /^[0-9a-z]+$/;
const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i;
const nanoidRegex = /^[a-z0-9_-]{21}$/i;
const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/;
const durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/;
const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i;
const _emojiRegex = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`;
let emojiRegex;
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
const ipv4CidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/;
const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
const ipv6CidrRegex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/;
const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;
const base64urlRegex = /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/;
const dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`;
const dateRegex = new RegExp(`^${dateRegexSource}$`);
function timeRegexSource(args) {
let secondsRegexSource = `[0-5]\\d`;
if (args.precision) {
secondsRegexSource = `${secondsRegexSource}\\.\\d{${args.precision}}`;
} else if (args.precision == null) {
secondsRegexSource = `${secondsRegexSource}(\\.\\d+)?`;
}
const secondsQuantifier = args.precision ? "+" : "?";
return `([01]\\d|2[0-3]):[0-5]\\d(:${secondsRegexSource})${secondsQuantifier}`;
}
function timeRegex(args) {
return new RegExp(`^${timeRegexSource(args)}$`);
}
function datetimeRegex(args) {
let regex = `${dateRegexSource}T${timeRegexSource(args)}`;
const opts = [];
opts.push(args.local ? `Z?` : `Z`);
if (args.offset)
opts.push(`([+-]\\d{2}:?\\d{2})`);
regex = `${regex}(${opts.join("|")})`;
return new RegExp(`^${regex}$`);
}
function isValidIP(ip, version) {
if ((version === "v4" || !version) && ipv4Regex.test(ip)) {
return true;
}
if ((version === "v6" || !version) && ipv6Regex.test(ip)) {
return true;
}
return false;
}
function isValidJWT(jwt, alg) {
if (!jwtRegex.test(jwt))
return false;
try {
const [header] = jwt.split(".");
if (!header)
return false;
const base64 = header.replace(/-/g, "+").replace(/_/g, "/").padEnd(header.length + (4 - header.length % 4) % 4, "=");
const decoded = JSON.parse(atob(base64));
if (typeof decoded !== "object" || decoded === null)
return false;
if ("typ" in decoded && (decoded == null ? void 0 : decoded.typ) !== "JWT")
return false;
if (!decoded.alg)
return false;
if (alg && decoded.alg !== alg)
return false;
return true;
} catch {
return false;
}
}
function isValidCidr(ip, version) {
if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) {
return true;
}
if ((version === "v6" || !version) && ipv6CidrRegex.test(ip)) {
return true;
}
return false;
}
class ZodString extends ZodType {
_parse(input) {
if (this._def.coerce) {
input.data = String(input.data);
}
const parsedType = this._getType(input);
if (parsedType !== ZodParsedType.string) {
const ctx2 = this._getOrReturnCtx(input);
addIssueToContext(ctx2, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.string,
received: ctx2.parsedType
});
return INVALID;
}
const status = new ParseStatus();
let ctx = void 0;
for (const check of this._def.checks) {
if (check.kind === "min") {
if (input.data.length < check.value) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.too_small,
minimum: check.value,
type: "string",
inclusive: true,
exact: false,
message: check.message
});
status.dirty();
}
} else if (check.kind === "max") {
if (input.data.length > check.value) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.too_big,
maximum: check.value,
type: "string",
inclusive: true,
exact: false,
message: check.message
});
status.dirty();
}
} else if (check.kind === "length") {
const tooBig = input.data.length > check.value;
const tooSmall = input.data.length < check.value;
if (tooBig || tooSmall) {
ctx = this._getOrReturnCtx(input, ctx);
if (tooBig) {
addIssueToContext(ctx, {
code: ZodIssueCode.too_big,
maximum: check.value,
type: "string",
inclusive: true,
exact: true,
message: check.message
});
} else if (tooSmall) {
addIssueToContext(ctx, {
code: ZodIssueCode.too_small,
minimum: check.value,
type: "string",
inclusive: true,
exact: true,
message: check.message
});
}
status.dirty();
}
} else if (check.kind === "email") {
if (!emailRegex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "email",
code: ZodIssueCode.invalid_string,
message: check.message
});
status.dirty();
}
} else if (check.kind === "emoji") {
if (!emojiRegex) {
emojiRegex = new RegExp(_emojiRegex, "u");
}
if (!emojiRegex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "emoji",
code: ZodIssueCode.invalid_string,
message: check.message
});
status.dirty();
}
} else if (check.kind === "uuid") {
if (!uuidRegex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "uuid",
code: ZodIssueCode.invalid_string,
message: check.message
});
status.dirty();
}
} else if (check.kind === "nanoid") {
if (!nanoidRegex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "nanoid",
code: ZodIssueCode.invalid_string,
message: check.message
});
status.dirty();
}
} else if (check.kind === "cuid") {
if (!cuidRegex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "cuid",
code: ZodIssueCode.invalid_string,
message: check.message
});
status.dirty();
}
} else if (check.kind === "cuid2") {
if (!cuid2Regex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "cuid2",
code: ZodIssueCode.invalid_string,
message: check.message
});
status.dirty();
}
} else if (check.kind === "ulid") {
if (!ulidRegex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "ulid",
code: ZodIssueCode.invalid_string,
message: check.message
});
status.dirty();
}
} else if (check.kind === "url") {
try {
new URL(input.data);
} catch {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "url",
code: ZodIssueCode.invalid_string,
message: check.message
});
status.dirty();
}
} else if (check.kind === "regex") {
check.regex.lastIndex = 0;
const testResult = check.regex.test(input.data);
if (!testResult) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "regex",
code: ZodIssueCode.invalid_string,
message: check.message
});
status.dirty();
}
} else if (check.kind === "trim") {
input.data = input.data.trim();
} else if (check.kind === "includes") {
if (!input.data.includes(check.value, check.position)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_string,
validation: { includes: check.value, position: check.position },
message: check.message
});
status.dirty();
}
} else if (check.kind === "toLowerCase") {
input.data = input.data.toLowerCase();
} else if (check.kind === "toUpperCase") {
input.data = input.data.toUpperCase();
} else if (check.kind === "startsWith") {
if (!input.data.startsWith(check.value)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_string,
validation: { startsWith: check.value },
message: check.message
});
status.dirty();
}
} else if (check.kind === "endsWith") {
if (!input.data.endsWith(check.value)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_string,
validation: { endsWith: check.value },
message: check.message
});
status.dirty();
}
} else if (check.kind === "datetime") {
const regex = datetimeRegex(check);
if (!regex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_string,
validation: "datetime",
message: check.message
});
status.dirty();
}
} else if (check.kind === "date") {
const regex = dateRegex;
if (!regex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_string,
validation: "date",
message: check.message
});
status.dirty();
}
} else if (check.kind === "time") {
const regex = timeRegex(check);
if (!regex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_string,
validation: "time",
message: check.message
});
status.dirty();
}
} else if (check.kind === "duration") {
if (!durationRegex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "duration",
code: ZodIssueCode.invalid_string,
message: check.message
});
status.dirty();
}
} else if (check.kind === "ip") {
if (!isValidIP(input.data, check.version)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "ip",
code: ZodIssueCode.invalid_string,
message: check.message
});
status.dirty();
}
} else if (check.kind === "jwt") {
if (!isValidJWT(input.data, check.alg)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "jwt",
code: ZodIssueCode.invalid_string,
message: check.message
});
status.dirty();
}
} else if (check.kind === "cidr") {
if (!isValidCidr(input.data, check.version)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "cidr",
code: ZodIssueCode.invalid_string,
message: check.message
});
status.dirty();
}
} else if (check.kind === "base64") {
if (!base64Regex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "base64",
code: ZodIssueCode.invalid_string,
message: check.message
});
status.dirty();
}
} else if (check.kind === "base64url") {
if (!base64urlRegex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "base64url",
code: ZodIssueCode.invalid_string,
message: check.message
});
status.dirty();
}
} else {
util.assertNever(check);
}
}
return { status: status.value, value: input.data };
}
_regex(regex, validation, message) {
return this.refinement((data) => regex.test(data), {
validation,
code: ZodIssueCode.invalid_string,
...errorUtil.errToObj(message)
});
}
_addCheck(check) {
return new ZodString({
...this._def,
checks: [...this._def.checks, check]
});
}
email(message) {
return this._addCheck({ kind: "email", ...errorUtil.errToObj(message) });
}
url(message) {
return this._addCheck({ kind: "url", ...errorUtil.errToObj(message) });
}
emoji(message) {
return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(message) });
}
uuid(message) {
return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(message) });
}
nanoid(message) {
return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(message) });
}
cuid(message) {
return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(message) });
}
cuid2(message) {
return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) });
}
ulid(message) {
return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message) });
}
base64(message) {
return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) });
}
base64url(message) {
return this._addCheck({
kind: "base64url",
...errorUtil.errToObj(message)
});
}
jwt(options) {
return this._addCheck({ kind: "jwt", ...errorUtil.errToObj(options) });
}
ip(options) {
return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) });
}
cidr(options) {
return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) });
}
datetime(options) {
if (typeof options === "string") {
return this._addCheck({
kind: "datetime",
precision: null,
offset: false,
local: false,
message: options
});
}
return this._addCheck({
kind: "datetime",
precision: typeof (options == null ? void 0 : options.precision) === "undefined" ? null : options == null ? void 0 : options.precision,
offset: (options == null ? void 0 : options.offset) ?? false,
local: (options == null ? void 0 : options.local) ?? false,
...errorUtil.errToObj(options == null ? void 0 : options.message)
});
}
date(message) {
return this._addCheck({ kind: "date", message });
}
time(options) {
if (typeof options === "string") {
return this._addCheck({
kind: "time",
precision: null,
message: options
});
}
return this._addCheck({
kind: "time",
precision: typeof (options == null ? void 0 : options.precision) === "undefined" ? null : options == null ? void 0 : options.precision,
...errorUtil.errToObj(options == null ? void 0 : options.message)
});
}
duration(message) {
return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message) });
}
regex(regex, message) {
return this._addCheck({
kind: "regex",
regex,
...errorUtil.errToObj(message)
});
}
includes(value, options) {
return this._addCheck({
kind: "includes",
value,
position: options == null ? void 0 : options.position,
...errorUtil.errToObj(options == null ? void 0 : options.message)
});
}
startsWith(value, message) {
return this._addCheck({
kind: "startsWith",
value,
...errorUtil.errToObj(message)
});
}
endsWith(value, message) {
return this._addCheck({
kind: "endsWith",
value,
...errorUtil.errToObj(message)
});
}
min(minLength, message) {
return this._addCheck({
kind: "min",
value: minLength,
...errorUtil.errToObj(message)
});
}
max(maxLength, message) {
return this._addCheck({
kind: "max",
value: maxLength,
...errorUtil.errToObj(message)
});
}
length(len, message) {
return this._addCheck({
kind: "length",
value: len,
...errorUtil.errToObj(message)
});
}
/**
* Equivalent to `.min(1)`
*/
nonempty(message) {
return this.min(1, errorUtil.errToObj(message));
}
trim() {
return new ZodString({
...this._def,
checks: [...this._def.checks, { kind: "trim" }]
});
}
toLowerCase() {
return new ZodString({
...this._def,
checks: [...this._def.checks, { kind: "toLowerCase" }]
});
}
toUpperCase() {
return new ZodString({
...this._def,
checks: [...this._def.checks, { kind: "toUpperCase" }]
});
}
get isDatetime() {
return !!this._def.checks.find((ch) => ch.kind === "datetime");
}
get isDate() {
return !!this._def.checks.find((ch) => ch.kind === "date");
}
get isTime() {
return !!this._def.checks.find((ch) => ch.kind === "time");
}
get isDuration() {
return !!this._def.checks.find((ch) => ch.kind === "duration");
}
get isEmail() {
return !!this._def.checks.find((ch) => ch.kind === "email");
}
get isURL() {
return !!this._def.checks.find((ch) => ch.kind === "url");
}
get isEmoji() {
return !!this._def.checks.find((ch) => ch.kind === "emoji");
}
get isUUID() {
return !!this._def.checks.find((ch) => ch.kind === "uuid");
}
get isNANOID() {
return !!this._def.checks.find((ch) => ch.kind === "nanoid");
}
get isCUID() {
return !!this._def.checks.find((ch) => ch.kind === "cuid");
}
get isCUID2() {
return !!this._def.checks.find((ch) => ch.kind === "cuid2");
}
get isULID() {
return !!this._def.checks.find((ch) => ch.kind === "ulid");
}
get isIP() {
return !!this._def.checks.find((ch) => ch.kind === "ip");
}
get isCIDR() {
return !!this._def.checks.find((ch) => ch.kind === "cidr");
}
get isBase64() {
return !!this._def.checks.find((ch) => ch.kind === "base64");
}
get isBase64url() {
return !!this._def.checks.find((ch) => ch.kind === "base64url");
}
get minLength() {
let min = null;
for (const ch of this._def.checks) {
if (ch.kind === "min") {
if (min === null || ch.value > min)
min = ch.value;
}
}
return min;
}
get maxLength() {
let max = null;
for (const ch of this._def.checks) {
if (ch.kind === "max") {
if (max === null || ch.value < max)
max = ch.value;
}
}
return max;
}
}
ZodString.create = (params) => {
return new ZodString({
checks: [],
typeName: ZodFirstPartyTypeKind.ZodString,
coerce: (params == null ? void 0 : params.coerce) ?? false,
...processCreateParams(params)
});
};
function floatSafeRemainder(val, step) {
const valDecCount = (val.toString().split(".")[1] || "").length;
const stepDecCount = (step.toString().split(".")[1] || "").length;
const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount;
const valInt = Number.parseInt(val.toFixed(decCount).replace(".", ""));
const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", ""));
return valInt % stepInt / 10 ** decCount;
}
class ZodNumber extends ZodType {
constructor() {
super(...arguments);
this.min = this.gte;
this.max = this.lte;
this.step = this.multipleOf;
}
_parse(input) {
if (this._def.coerce) {
input.data = Number(input.data);
}
const parsedType = this._getType(input);
if (parsedType !== ZodParsedType.number) {
const ctx2 = this._getOrReturnCtx(input);
addIssueToContext(ctx2, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.number,
received: ctx2.parsedType
});
return INVALID;
}
let ctx = void 0;
const status = new ParseStatus();
for (const check of this._def.checks) {
if (check.kind === "int") {
if (!util.isInteger(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: "integer",
received: "float",
message: check.message
});
status.dirty();
}
} else if (check.kind === "min") {
const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value;
if (tooSmall) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.too_small,
minimum: check.value,
type: "number",
inclusive: check.inclusive,
exact: false,
message: check.message
});
status.dirty();
}
} else if (check.kind === "max") {
const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value;
if (tooBig) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.too_big,
maximum: check.value,
type: "number",
inclusive: check.inclusive,
exact: false,
message: check.message
});
status.dirty();
}
} else if (check.kind === "multipleOf") {
if (floatSafeRemainder(input.data, check.value) !== 0) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.not_multiple_of,
multipleOf: check.value,
message: check.message
});
status.dirty();
}
} else if (check.kind === "finite") {
if (!Number.isFinite(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.not_finite,
message: check.message
});
status.dirty();
}
} else {
util.assertNever(check);
}
}
return { status: status.value, value: input.data };
}
gte(value, message) {
return this.setLimit("min", value, true, errorUtil.toString(message));
}
gt(value, message) {
return this.setLimit("min", value, false, errorUtil.toString(message));
}
lte(value, message) {
return this.setLimit("max", value, true, errorUtil.toString(message));
}
lt(value, message) {
return this.setLimit("max", value, false, errorUtil.toString(message));
}
setLimit(kind, value, inclusive, message) {
return new ZodNumber({
...this._def,
checks: [
...this._def.checks,
{
kind,
value,
inclusive,
message: errorUtil.toString(message)
}
]
});
}
_addCheck(check) {
return new ZodNumber({
...this._def,
checks: [...this._def.checks, check]
});
}
int(message) {
return this._addCheck({
kind: "int",
message: errorUtil.toString(message)
});
}
positive(message) {
return this._addCheck({
kind: "min",
value: 0,
inclusive: false,
message: errorUtil.toString(message)
});
}
negative(message) {
return this._addCheck({
kind: "max",
value: 0,
inclusive: false,
message: errorUtil.toString(message)
});
}
nonpositive(message) {
return this._addCheck({
kind: "max",
value: 0,
inclusive: true,
message: errorUtil.toString(message)
});
}
nonnegative(message) {
return this._addCheck({
kind: "min",
value: 0,
inclusive: true,
message: errorUtil.toString(message)
});
}
multipleOf(value, message) {
return this._addCheck({
kind: "multipleOf",
value,
message: errorUtil.toString(message)
});
}
finite(message) {
return this._addCheck({
kind: "finite",
message: errorUtil.toString(message)
});
}
safe(message) {
return this._addCheck({
kind: "min",
inclusive: true,
value: Number.MIN_SAFE_INTEGER,
message: errorUtil.toString(message)
})._addCheck({
kind: "max",
inclusive: true,
value: Number.MAX_SAFE_INTEGER,
message: errorUtil.toString(message)
});
}
get minValue() {
let min = null;
for (const ch of this._def.checks) {
if (ch.kind === "min") {
if (min === null || ch.value > min)
min = ch.value;
}
}
return min;
}
get maxValue() {
let max = null;
for (const ch of this._def.checks) {
if (ch.kind === "max") {
if (max === null || ch.value < max)
max = ch.value;
}
}
return max;
}
get isInt() {
return !!this._def.checks.find((ch) => ch.kind === "int" || ch.kind === "multipleOf" && util.isInteger(ch.value));
}
get isFinite() {
let max = null;
let min = null;
for (const ch of this._def.checks) {
if (ch.kind === "finite" || ch.kind === "int" || ch.kind === "multipleOf") {
return true;
} else if (ch.kind === "min") {
if (min === null || ch.value > min)
min = ch.value;
} else if (ch.kind === "max") {
if (max === null || ch.value < max)
max = ch.value;
}
}
return Number.isFinite(min) && Number.isFinite(max);
}
}
ZodNumber.create = (params) => {
return new ZodNumber({
checks: [],
typeName: ZodFirstPartyTypeKind.ZodNumber,
coerce: (params == null ? void 0 : params.coerce) || false,
...processCreateParams(params)
});
};
class ZodBigInt extends ZodType {
constructor() {
super(...arguments);
this.min = this.gte;
this.max = this.lte;
}
_parse(input) {
if (this._def.coerce) {
try {
input.data = BigInt(input.data);
} catch {
return this._getInvalidInput(input);
}
}
const parsedType = this._getType(input);
if (parsedType !== ZodParsedType.bigint) {
return this._getInvalidInput(input);
}
let ctx = void 0;
const status = new ParseStatus();
for (const check of this._def.checks) {
if (check.kind === "min") {
const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value;
if (tooSmall) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.too_small,
type: "bigint",
minimum: check.value,
inclusive: check.inclusive,
message: check.message
});
status.dirty();
}
} else if (check.kind === "max") {
const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value;
if (tooBig) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.too_big,
type: "bigint",
maximum: check.value,
inclusive: check.inclusive,
message: check.message
});
status.dirty();
}
} else if (check.kind === "multipleOf") {
if (input.data % check.value !== BigInt(0)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.not_multiple_of,
multipleOf: check.value,
message: check.message
});
status.dirty();
}
} else {
util.assertNever(check);
}
}
return { status: status.value, value: input.data };
}
_getInvalidInput(input) {
const ctx = this._getOrReturnCtx(input);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.bigint,
received: ctx.parsedType
});
return INVALID;
}
gte(value, message) {
return this.setLimit("min", value, true, errorUtil.toString(message));
}
gt(value, message) {
return this.setLimit("min", value, false, errorUtil.toString(message));
}
lte(value, message) {
return this.setLimit("max", value, true, errorUtil.toString(message));
}
lt(value, message) {
return this.setLimit("max", value, false, errorUtil.toString(message));
}
setLimit(kind, value, inclusive, message) {
return new ZodBigInt({
...this._def,
checks: [
...this._def.checks,
{
kind,
value,
inclusive,
message: errorUtil.toString(message)
}
]
});
}
_addCheck(check) {
return new ZodBigInt({
...this._def,
checks: [...this._def.checks, check]
});
}
positive(message) {
return this._addCheck({
kind: "min",
value: BigInt(0),
inclusive: false,
message: errorUtil.toString(message)
});
}
negative(message) {
return this._addCheck({
kind: "max",
value: BigInt(0),
inclusive: false,
message: errorUtil.toString(message)
});
}
nonpositive(message) {
return this._addCheck({
kind: "max",
value: BigInt(0),
inclusive: true,
message: errorUtil.toString(message)
});
}
nonnegative(message) {
return this._addCheck({
kind: "min",
value: BigInt(0),
inclusive: true,
message: errorUtil.toString(message)
});
}
multipleOf(value, message) {
return this._addCheck({
kind: "multipleOf",
value,
message: errorUtil.toString(message)
});
}
get minValue() {
let min = null;
for (const ch of this._def.checks) {
if (ch.kind === "min") {
if (min === null || ch.value > min)
min = ch.value;
}
}
return min;
}
get maxValue() {
let max = null;
for (const ch of this._def.checks) {
if (ch.kind === "max") {
if (max === null || ch.value < max)
max = ch.value;
}
}
return max;
}
}
ZodBigInt.create = (params) => {
return new ZodBigInt({
checks: [],
typeName: ZodFirstPartyTypeKind.ZodBigInt,
coerce: (params == null ? void 0 : params.coerce) ?? false,
...processCreateParams(params)
});
};
class ZodBoolean extends ZodType {
_parse(input) {
if (this._def.coerce) {
input.data = Boolean(input.data);
}
const parsedType = this._getType(input);
if (parsedType !== ZodParsedType.boolean) {
const ctx = this._getOrReturnCtx(input);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.boolean,
received: ctx.parsedType
});
return INVALID;
}
return OK(input.data);
}
}
ZodBoolean.create = (params) => {
return new ZodBoolean({
typeName: ZodFirstPartyTypeKind.ZodBoolean,
coerce: (params == null ? void 0 : params.coerce) || false,
...processCreateParams(params)
});
};
class ZodDate extends ZodType {
_parse(input) {
if (this._def.coerce) {
input.data = new Date(input.data);
}
const parsedType = this._getType(input);
if (parsedType !== ZodParsedType.date) {
const ctx2 = this._getOrReturnCtx(input);
addIssueToContext(ctx2, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.date,
received: ctx2.parsedType
});
return INVALID;
}
if (Number.isNaN(input.data.getTime())) {
const ctx2 = this._getOrReturnCtx(input);
addIssueToContext(ctx2, {
code: ZodIssueCode.invalid_date
});
return INVALID;
}
const status = new ParseStatus();
let ctx = void 0;
for (const check of this._def.checks) {
if (check.kind === "min") {
if (input.data.getTime() < check.value) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.too_small,
message: check.message,
inclusive: true,
exact: false,
minimum: check.value,
type: "date"
});
status.dirty();
}
} else if (check.kind === "max") {
if (input.data.getTime() > check.value) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.too_big,
message: check.message,
inclusive: true,
exact: false,
maximum: check.value,
type: "date"
});
status.dirty();
}
} else {
util.assertNever(check);
}
}
return {
status: status.value,
value: new Date(input.data.getTime())
};
}
_addCheck(check) {
return new ZodDate({
...this._def,
checks: [...this._def.checks, check]
});
}
min(minDate, message) {
return this._addCheck({
kind: "min",
value: minDate.getTime(),
message: errorUtil.toString(message)
});
}
max(maxDate, message) {
return this._addCheck({
kind: "max",
value: maxDate.getTime(),
message: errorUtil.toString(message)
});
}
get minDate() {
let min = null;
for (const ch of this._def.checks) {
if (ch.kind === "min") {
if (min === null || ch.value > min)
min = ch.value;
}
}
return min != null ? new Date(min) : null;
}
get maxDate() {
let max = null;
for (const ch of this._def.checks) {
if (ch.kind === "max") {
if (max === null || ch.value < max)
max = ch.value;
}
}
return max != null ? new Date(max) : null;
}
}
ZodDate.create = (params) => {
return new ZodDate({
checks: [],
coerce: (params == null ? void 0 : params.coerce) || false,
typeName: ZodFirstPartyTypeKind.ZodDate,
...processCreateParams(params)
});
};
class ZodSymbol extends ZodType {
_parse(input) {
const parsedType = this._getType(input);
if (parsedType !== ZodParsedType.symbol) {
const ctx = this._getOrReturnCtx(input);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.symbol,
received: ctx.parsedType
});
return INVALID;
}
return OK(input.data);
}
}
ZodSymbol.create = (params) => {
return new ZodSymbol({
typeName: ZodFirstPartyTypeKind.ZodSymbol,
...processCreateParams(params)
});
};
class ZodUndefined extends ZodType {
_parse(input) {
const parsedType = this._getType(input);
if (parsedType !== ZodParsedType.undefined) {
const ctx = this._getOrReturnCtx(input);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.undefined,
received: ctx.parsedType
});
return INVALID;
}
return OK(input.data);
}
}
ZodUndefined.create = (params) => {
return new ZodUndefined({
typeName: ZodFirstPartyTypeKind.ZodUndefined,
...processCreateParams(params)
});
};
class ZodNull extends ZodType {
_parse(input) {
const parsedType = this._getType(input);
if (parsedType !== ZodParsedType.null) {
const ctx = this._getOrReturnCtx(input);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.null,
received: ctx.parsedType
});
return INVALID;
}
return OK(input.data);
}
}
ZodNull.create = (params) => {
return new ZodNull({
typeName: ZodFirstPartyTypeKind.ZodNull,
...processCreateParams(params)
});
};
class ZodAny extends ZodType {
constructor() {
super(...arguments);
this._any = true;
}
_parse(input) {
return OK(input.data);
}
}
ZodAny.create = (params) => {
return new ZodAny({
typeName: ZodFirstPartyTypeKind.ZodAny,
...processCreateParams(params)
});
};
class ZodUnknown extends ZodType {
constructor() {
super(...arguments);
this._unknown = true;
}
_parse(input) {
return OK(input.data);
}
}
ZodUnknown.create = (params) => {
return new ZodUnknown({
typeName: ZodFirstPartyTypeKind.ZodUnknown,
...processCreateParams(params)
});
};
class ZodNever extends ZodType {
_parse(input) {
const ctx = this._getOrReturnCtx(input);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.never,
received: ctx.parsedType
});
return INVALID;
}
}
ZodNever.create = (params) => {
return new ZodNever({
typeName: ZodFirstPartyTypeKind.ZodNever,
...processCreateParams(params)
});
};
class ZodVoid extends ZodType {
_parse(input) {
const parsedType = this._getType(input);
if (parsedType !== ZodParsedType.undefined) {
const ctx = this._getOrReturnCtx(input);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.void,
received: ctx.parsedType
});
return INVALID;
}
return OK(input.data);
}
}
ZodVoid.create = (params) => {
return new ZodVoid({
typeName: ZodFirstPartyTypeKind.ZodVoid,
...processCreateParams(params)
});
};
class ZodArray extends ZodType {
_parse(input) {
const { ctx, status } = this._processInputParams(input);
const def = this._def;
if (ctx.parsedType !== ZodParsedType.array) {
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.array,
received: ctx.parsedType
});
return INVALID;
}
if (def.exactLength !== null) {
const tooBig = ctx.data.length > def.exactLength.value;
const tooSmall = ctx.data.length < def.exactLength.value;
if (tooBig || tooSmall) {
addIssueToContext(ctx, {
code: tooBig ? ZodIssueCode.too_big : ZodIssueCode.too_small,
minimum: tooSmall ? def.exactLength.value : void 0,
maximum: tooBig ? def.exactLength.value : void 0,
type: "array",
inclusive: true,
exact: true,
message: def.exactLength.message
});
status.dirty();
}
}
if (def.minLength !== null) {
if (ctx.data.length < def.minLength.value) {
addIssueToContext(ctx, {
code: ZodIssueCode.too_small,
minimum: def.minLength.value,
type: "array",
inclusive: true,
exact: false,
message: def.minLength.message
});
status.dirty();
}
}
if (def.maxLength !== null) {
if (ctx.data.length > def.maxLength.value) {
addIssueToContext(ctx, {
code: ZodIssueCode.too_big,
maximum: def.maxLength.value,
type: "array",
inclusive: true,
exact: false,
message: def.maxLength.message
});
status.dirty();
}
}
if (ctx.common.async) {
return Promise.all([...ctx.data].map((item, i) => {
return def.type._parseAsync(new ParseInputLazyPath(ctx, item, ctx.path, i));
})).then((result2) => {
return ParseStatus.mergeArray(status, result2);
});
}
const result = [...ctx.data].map((item, i) => {
return def.type._parseSync(new ParseInputLazyPath(ctx, item, ctx.path, i));
});
return ParseStatus.mergeArray(status, result);
}
get element() {
return this._def.type;
}
min(minLength, message) {
return new ZodArray({
...this._def,
minLength: { value: minLength, message: errorUtil.toString(message) }
});
}
max(maxLength, message) {
return new ZodArray({
...this._def,
maxLength: { value: maxLength, message: errorUtil.toString(message) }
});
}
length(len, message) {
return new ZodArray({
...this._def,
exactLength: { value: len, message: errorUtil.toString(message) }
});
}
nonempty(message) {
return this.min(1, message);
}
}
ZodArray.create = (schema, params) => {
return new ZodArray({
type: schema,
minLength: null,
maxLength: null,
exactLength: null,
typeName: ZodFirstPartyTypeKind.ZodArray,
...processCreateParams(params)
});
};
function deepPartialify(schema) {
if (schema instanceof ZodObject) {
const newShape = {};
for (const key in schema.shape) {
const fieldSchema = schema.shape[key];
newShape[key] = ZodOptional.create(deepPartialify(fieldSchema));
}
return new ZodObject({
...schema._def,
shape: () => newShape
});
} else if (schema instanceof ZodArray) {
return new ZodArray({
...schema._def,
type: deepPartialify(schema.element)
});
} else if (schema instanceof ZodOptional) {
return ZodOptional.create(deepPartialify(schema.unwrap()));
} else if (schema instanceof ZodNullable) {
return ZodNullable.create(deepPartialify(schema.unwrap()));
} else if (schema instanceof ZodTuple) {
return ZodTuple.create(schema.items.map((item) => deepPartialify(item)));
} else {
return schema;
}
}
class ZodObject extends ZodType {
constructor() {
super(...arguments);
this._cached = null;
this.nonstrict = this.passthrough;
this.augment = this.extend;
}
_getCached() {
if (this._cached !== null)
return this._cached;
const shape = this._def.shape();
const keys = util.objectKeys(shape);
this._cached = { shape, keys };
return this._cached;
}
_parse(input) {
const parsedType = this._getType(input);
if (parsedType !== ZodParsedType.object) {
const ctx2 = this._getOrReturnCtx(input);
addIssueToContext(ctx2, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.object,
received: ctx2.parsedType
});
return INVALID;
}
const { status, ctx } = this._processInputParams(input);
const { shape, keys: shapeKeys } = this._getCached();
const extraKeys = [];
if (!(this._def.catchall instanceof ZodNever && this._def.unknownKeys === "strip")) {
for (const key in ctx.data) {
if (!shapeKeys.includes(key)) {
extraKeys.push(key);
}
}
}
const pairs = [];
for (const key of shapeKeys) {
const keyValidator = shape[key];
const value = ctx.data[key];
pairs.push({
key: { status: "valid", value: key },
value: keyValidator._parse(new ParseInputLazyPath(ctx, value, ctx.path, key)),
alwaysSet: key in ctx.data
});
}
if (this._def.catchall instanceof ZodNever) {
const unknownKeys = this._def.unknownKeys;
if (unknownKeys === "passthrough") {
for (const key of extraKeys) {
pairs.push({
key: { status: "valid", value: key },
value: { status: "valid", value: ctx.data[key] }
});
}
} else if (unknownKeys === "strict") {
if (extraKeys.length > 0) {
addIssueToContext(ctx, {
code: ZodIssueCode.unrecognized_keys,
keys: extraKeys
});
status.dirty();
}
} else if (unknownKeys === "strip") ;
else {
throw new Error(`Internal ZodObject error: invalid unknownKeys value.`);
}
} else {
const catchall = this._def.catchall;
for (const key of extraKeys) {
const value = ctx.data[key];
pairs.push({
key: { status: "valid", value: key },
value: catchall._parse(
new ParseInputLazyPath(ctx, value, ctx.path, key)
//, ctx.child(key), value, getParsedType(value)
),
alwaysSet: key in ctx.data
});
}
}
if (ctx.common.async) {
return Promise.resolve().then(async () => {
const syncPairs = [];
for (const pair of pairs) {
const key = await pair.key;
const value = await pair.value;
syncPairs.push({
key,
value,
alwaysSet: pair.alwaysSet
});
}
return syncPairs;
}).then((syncPairs) => {
return ParseStatus.mergeObjectSync(status, syncPairs);
});
} else {
return ParseStatus.mergeObjectSync(status, pairs);
}
}
get shape() {
return this._def.shape();
}
strict(message) {
errorUtil.errToObj;
return new ZodObject({
...this._def,
unknownKeys: "strict",
...message !== void 0 ? {
errorMap: (issue, ctx) => {
var _a2, _b;
const defaultError = ((_b = (_a2 = this._def).errorMap) == null ? void 0 : _b.call(_a2, issue, ctx).message) ?? ctx.defaultError;
if (issue.code === "unrecognized_keys")
return {
message: errorUtil.errToObj(message).message ?? defaultError
};
return {
message: defaultError
};
}
} : {}
});
}
strip() {
return new ZodObject({
...this._def,
unknownKeys: "strip"
});
}
passthrough() {
return new ZodObject({
...this._def,
unknownKeys: "passthrough"
});
}
// const AugmentFactory =
// (def: Def) =>
// (
// augmentation: Augmentation
// ): ZodObject<
// extendShape, Augmentation>,
// Def["unknownKeys"],
// Def["catchall"]
// > => {
// return new ZodObject({
// ...def,
// shape: () => ({
// ...def.shape(),
// ...augmentation,
// }),
// }) as any;
// };
extend(augmentation) {
return new ZodObject({
...this._def,
shape: () => ({
...this._def.shape(),
...augmentation
})
});
}
/**
* Prior to zod@1.0.12 there was a bug in the
* inferred type of merged objects. Please
* upgrade if you are experiencing issues.
*/
merge(merging) {
const merged = new ZodObject({
unknownKeys: merging._def.unknownKeys,
catchall: merging._def.catchall,
shape: () => ({
...this._def.shape(),
...merging._def.shape()
}),
typeName: ZodFirstPartyTypeKind.ZodObject
});
return merged;
}
// merge<
// Incoming extends AnyZodObject,
// Augmentation extends Incoming["shape"],
// NewOutput extends {
// [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation
// ? Augmentation[k]["_output"]
// : k extends keyof Output
// ? Output[k]
// : never;
// },
// NewInput extends {
// [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation
// ? Augmentation[k]["_input"]
// : k extends keyof Input
// ? Input[k]
// : never;
// }
// >(
// merging: Incoming
// ): ZodObject<
// extendShape>,
// Incoming["_def"]["unknownKeys"],
// Incoming["_def"]["catchall"],
// NewOutput,
// NewInput
// > {
// const merged: any = new ZodObject({
// unknownKeys: merging._def.unknownKeys,
// catchall: merging._def.catchall,
// shape: () =>
// objectUtil.mergeShapes(this._def.shape(), merging._def.shape()),
// typeName: ZodFirstPartyTypeKind.ZodObject,
// }) as any;
// return merged;
// }
setKey(key, schema) {
return this.augment({ [key]: schema });
}
// merge(
// merging: Incoming
// ): //ZodObject = (merging) => {
// ZodObject<
// extendShape>,
// Incoming["_def"]["unknownKeys"],
// Incoming["_def"]["catchall"]
// > {
// // const mergedShape = objectUtil.mergeShapes(
// // this._def.shape(),
// // merging._def.shape()
// // );
// const merged: any = new ZodObject({
// unknownKeys: merging._def.unknownKeys,
// catchall: merging._def.catchall,
// shape: () =>
// objectUtil.mergeShapes(this._def.shape(), merging._def.shape()),
// typeName: ZodFirstPartyTypeKind.ZodObject,
// }) as any;
// return merged;
// }
catchall(index) {
return new ZodObject({
...this._def,
catchall: index
});
}
pick(mask) {
const shape = {};
for (const key of util.objectKeys(mask)) {
if (mask[key] && this.shape[key]) {
shape[key] = this.shape[key];
}
}
return new ZodObject({
...this._def,
shape: () => shape
});
}
omit(mask) {
const shape = {};
for (const key of util.objectKeys(this.shape)) {
if (!mask[key]) {
shape[key] = this.shape[key];
}
}
return new ZodObject({
...this._def,
shape: () => shape
});
}
/**
* @deprecated
*/
deepPartial() {
return deepPartialify(this);
}
partial(mask) {
const newShape = {};
for (const key of util.objectKeys(this.shape)) {
const fieldSchema = this.shape[key];
if (mask && !mask[key]) {
newShape[key] = fieldSchema;
} else {
newShape[key] = fieldSchema.optional();
}
}
return new ZodObject({
...this._def,
shape: () => newShape
});
}
required(mask) {
const newShape = {};
for (const key of util.objectKeys(this.shape)) {
if (mask && !mask[key]) {
newShape[key] = this.shape[key];
} else {
const fieldSchema = this.shape[key];
let newField = fieldSchema;
while (newField instanceof ZodOptional) {
newField = newField._def.innerType;
}
newShape[key] = newField;
}
}
return new ZodObject({
...this._def,
shape: () => newShape
});
}
keyof() {
return createZodEnum(util.objectKeys(this.shape));
}
}
ZodObject.create = (shape, params) => {
return new ZodObject({
shape: () => shape,
unknownKeys: "strip",
catchall: ZodNever.create(),
typeName: ZodFirstPartyTypeKind.ZodObject,
...processCreateParams(params)
});
};
ZodObject.strictCreate = (shape, params) => {
return new ZodObject({
shape: () => shape,
unknownKeys: "strict",
catchall: ZodNever.create(),
typeName: ZodFirstPartyTypeKind.ZodObject,
...processCreateParams(params)
});
};
ZodObject.lazycreate = (shape, params) => {
return new ZodObject({
shape,
unknownKeys: "strip",
catchall: ZodNever.create(),
typeName: ZodFirstPartyTypeKind.ZodObject,
...processCreateParams(params)
});
};
class ZodUnion extends ZodType {
_parse(input) {
const { ctx } = this._processInputParams(input);
const options = this._def.options;
function handleResults(results) {
for (const result of results) {
if (result.result.status === "valid") {
return result.result;
}
}
for (const result of results) {
if (result.result.status === "dirty") {
ctx.common.issues.push(...result.ctx.common.issues);
return result.result;
}
}
const unionErrors = results.map((result) => new ZodError(result.ctx.common.issues));
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_union,
unionErrors
});
return INVALID;
}
if (ctx.common.async) {
return Promise.all(options.map(async (option) => {
const childCtx = {
...ctx,
common: {
...ctx.common,
issues: []
},
parent: null
};
return {
result: await option._parseAsync({
data: ctx.data,
path: ctx.path,
parent: childCtx
}),
ctx: childCtx
};
})).then(handleResults);
} else {
let dirty = void 0;
const issues = [];
for (const option of options) {
const childCtx = {
...ctx,
common: {
...ctx.common,
issues: []
},
parent: null
};
const result = option._parseSync({
data: ctx.data,
path: ctx.path,
parent: childCtx
});
if (result.status === "valid") {
return result;
} else if (result.status === "dirty" && !dirty) {
dirty = { result, ctx: childCtx };
}
if (childCtx.common.issues.length) {
issues.push(childCtx.common.issues);
}
}
if (dirty) {
ctx.common.issues.push(...dirty.ctx.common.issues);
return dirty.result;
}
const unionErrors = issues.map((issues2) => new ZodError(issues2));
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_union,
unionErrors
});
return INVALID;
}
}
get options() {
return this._def.options;
}
}
ZodUnion.create = (types, params) => {
return new ZodUnion({
options: types,
typeName: ZodFirstPartyTypeKind.ZodUnion,
...processCreateParams(params)
});
};
const getDiscriminator = (type) => {
if (type instanceof ZodLazy) {
return getDiscriminator(type.schema);
} else if (type instanceof ZodEffects) {
return getDiscriminator(type.innerType());
} else if (type instanceof ZodLiteral) {
return [type.value];
} else if (type instanceof ZodEnum) {
return type.options;
} else if (type instanceof ZodNativeEnum) {
return util.objectValues(type.enum);
} else if (type instanceof ZodDefault) {
return getDiscriminator(type._def.innerType);
} else if (type instanceof ZodUndefined) {
return [void 0];
} else if (type instanceof ZodNull) {
return [null];
} else if (type instanceof ZodOptional) {
return [void 0, ...getDiscriminator(type.unwrap())];
} else if (type instanceof ZodNullable) {
return [null, ...getDiscriminator(type.unwrap())];
} else if (type instanceof ZodBranded) {
return getDiscriminator(type.unwrap());
} else if (type instanceof ZodReadonly) {
return getDiscriminator(type.unwrap());
} else if (type instanceof ZodCatch) {
return getDiscriminator(type._def.innerType);
} else {
return [];
}
};
class ZodDiscriminatedUnion extends ZodType {
_parse(input) {
const { ctx } = this._processInputParams(input);
if (ctx.parsedType !== ZodParsedType.object) {
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.object,
received: ctx.parsedType
});
return INVALID;
}
const discriminator = this.discriminator;
const discriminatorValue = ctx.data[discriminator];
const option = this.optionsMap.get(discriminatorValue);
if (!option) {
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_union_discriminator,
options: Array.from(this.optionsMap.keys()),
path: [discriminator]
});
return INVALID;
}
if (ctx.common.async) {
return option._parseAsync({
data: ctx.data,
path: ctx.path,
parent: ctx
});
} else {
return option._parseSync({
data: ctx.data,
path: ctx.path,
parent: ctx
});
}
}
get discriminator() {
return this._def.discriminator;
}
get options() {
return this._def.options;
}
get optionsMap() {
return this._def.optionsMap;
}
/**
* The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor.
* However, it only allows a union of objects, all of which need to share a discriminator property. This property must
* have a different value for each object in the union.
* @param discriminator the name of the discriminator property
* @param types an array of object schemas
* @param params
*/
static create(discriminator, options, params) {
const optionsMap = /* @__PURE__ */ new Map();
for (const type of options) {
const discriminatorValues = getDiscriminator(type.shape[discriminator]);
if (!discriminatorValues.length) {
throw new Error(`A discriminator value for key \`${discriminator}\` could not be extracted from all schema options`);
}
for (const value of discriminatorValues) {
if (optionsMap.has(value)) {
throw new Error(`Discriminator property ${String(discriminator)} has duplicate value ${String(value)}`);
}
optionsMap.set(value, type);
}
}
return new ZodDiscriminatedUnion({
typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion,
discriminator,
options,
optionsMap,
...processCreateParams(params)
});
}
}
function mergeValues(a, b) {
const aType = getParsedType(a);
const bType = getParsedType(b);
if (a === b) {
return { valid: true, data: a };
} else if (aType === ZodParsedType.object && bType === ZodParsedType.object) {
const bKeys = util.objectKeys(b);
const sharedKeys = util.objectKeys(a).filter((key) => bKeys.indexOf(key) !== -1);
const newObj = { ...a, ...b };
for (const key of sharedKeys) {
const sharedValue = mergeValues(a[key], b[key]);
if (!sharedValue.valid) {
return { valid: false };
}
newObj[key] = sharedValue.data;
}
return { valid: true, data: newObj };
} else if (aType === ZodParsedType.array && bType === ZodParsedType.array) {
if (a.length !== b.length) {
return { valid: false };
}
const newArray = [];
for (let index = 0; index < a.length; index++) {
const itemA = a[index];
const itemB = b[index];
const sharedValue = mergeValues(itemA, itemB);
if (!sharedValue.valid) {
return { valid: false };
}
newArray.push(sharedValue.data);
}
return { valid: true, data: newArray };
} else if (aType === ZodParsedType.date && bType === ZodParsedType.date && +a === +b) {
return { valid: true, data: a };
} else {
return { valid: false };
}
}
class ZodIntersection extends ZodType {
_parse(input) {
const { status, ctx } = this._processInputParams(input);
const handleParsed = (parsedLeft, parsedRight) => {
if (isAborted(parsedLeft) || isAborted(parsedRight)) {
return INVALID;
}
const merged = mergeValues(parsedLeft.value, parsedRight.value);
if (!merged.valid) {
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_intersection_types
});
return INVALID;
}
if (isDirty(parsedLeft) || isDirty(parsedRight)) {
status.dirty();
}
return { status: status.value, value: merged.data };
};
if (ctx.common.async) {
return Promise.all([
this._def.left._parseAsync({
data: ctx.data,
path: ctx.path,
parent: ctx
}),
this._def.right._parseAsync({
data: ctx.data,
path: ctx.path,
parent: ctx
})
]).then(([left, right]) => handleParsed(left, right));
} else {
return handleParsed(this._def.left._parseSync({
data: ctx.data,
path: ctx.path,
parent: ctx
}), this._def.right._parseSync({
data: ctx.data,
path: ctx.path,
parent: ctx
}));
}
}
}
ZodIntersection.create = (left, right, params) => {
return new ZodIntersection({
left,
right,
typeName: ZodFirstPartyTypeKind.ZodIntersection,
...processCreateParams(params)
});
};
class ZodTuple extends ZodType {
_parse(input) {
const { status, ctx } = this._processInputParams(input);
if (ctx.parsedType !== ZodParsedType.array) {
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.array,
received: ctx.parsedType
});
return INVALID;
}
if (ctx.data.length < this._def.items.length) {
addIssueToContext(ctx, {
code: ZodIssueCode.too_small,
minimum: this._def.items.length,
inclusive: true,
exact: false,
type: "array"
});
return INVALID;
}
const rest = this._def.rest;
if (!rest && ctx.data.length > this._def.items.length) {
addIssueToContext(ctx, {
code: ZodIssueCode.too_big,
maximum: this._def.items.length,
inclusive: true,
exact: false,
type: "array"
});
status.dirty();
}
const items = [...ctx.data].map((item, itemIndex) => {
const schema = this._def.items[itemIndex] || this._def.rest;
if (!schema)
return null;
return schema._parse(new ParseInputLazyPath(ctx, item, ctx.path, itemIndex));
}).filter((x) => !!x);
if (ctx.common.async) {
return Promise.all(items).then((results) => {
return ParseStatus.mergeArray(status, results);
});
} else {
return ParseStatus.mergeArray(status, items);
}
}
get items() {
return this._def.items;
}
rest(rest) {
return new ZodTuple({
...this._def,
rest
});
}
}
ZodTuple.create = (schemas, params) => {
if (!Array.isArray(schemas)) {
throw new Error("You must pass an array of schemas to z.tuple([ ... ])");
}
return new ZodTuple({
items: schemas,
typeName: ZodFirstPartyTypeKind.ZodTuple,
rest: null,
...processCreateParams(params)
});
};
class ZodRecord extends ZodType {
get keySchema() {
return this._def.keyType;
}
get valueSchema() {
return this._def.valueType;
}
_parse(input) {
const { status, ctx } = this._processInputParams(input);
if (ctx.parsedType !== ZodParsedType.object) {
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.object,
received: ctx.parsedType
});
return INVALID;
}
const pairs = [];
const keyType = this._def.keyType;
const valueType = this._def.valueType;
for (const key in ctx.data) {
pairs.push({
key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, key)),
value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)),
alwaysSet: key in ctx.data
});
}
if (ctx.common.async) {
return ParseStatus.mergeObjectAsync(status, pairs);
} else {
return ParseStatus.mergeObjectSync(status, pairs);
}
}
get element() {
return this._def.valueType;
}
static create(first, second, third) {
if (second instanceof ZodType) {
return new ZodRecord({
keyType: first,
valueType: second,
typeName: ZodFirstPartyTypeKind.ZodRecord,
...processCreateParams(third)
});
}
return new ZodRecord({
keyType: ZodString.create(),
valueType: first,
typeName: ZodFirstPartyTypeKind.ZodRecord,
...processCreateParams(second)
});
}
}
class ZodMap extends ZodType {
get keySchema() {
return this._def.keyType;
}
get valueSchema() {
return this._def.valueType;
}
_parse(input) {
const { status, ctx } = this._processInputParams(input);
if (ctx.parsedType !== ZodParsedType.map) {
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.map,
received: ctx.parsedType
});
return INVALID;
}
const keyType = this._def.keyType;
const valueType = this._def.valueType;
const pairs = [...ctx.data.entries()].map(([key, value], index) => {
return {
key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index, "key"])),
value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index, "value"]))
};
});
if (ctx.common.async) {
const finalMap = /* @__PURE__ */ new Map();
return Promise.resolve().then(async () => {
for (const pair of pairs) {
const key = await pair.key;
const value = await pair.value;
if (key.status === "aborted" || value.status === "aborted") {
return INVALID;
}
if (key.status === "dirty" || value.status === "dirty") {
status.dirty();
}
finalMap.set(key.value, value.value);
}
return { status: status.value, value: finalMap };
});
} else {
const finalMap = /* @__PURE__ */ new Map();
for (const pair of pairs) {
const key = pair.key;
const value = pair.value;
if (key.status === "aborted" || value.status === "aborted") {
return INVALID;
}
if (key.status === "dirty" || value.status === "dirty") {
status.dirty();
}
finalMap.set(key.value, value.value);
}
return { status: status.value, value: finalMap };
}
}
}
ZodMap.create = (keyType, valueType, params) => {
return new ZodMap({
valueType,
keyType,
typeName: ZodFirstPartyTypeKind.ZodMap,
...processCreateParams(params)
});
};
class ZodSet extends ZodType {
_parse(input) {
const { status, ctx } = this._processInputParams(input);
if (ctx.parsedType !== ZodParsedType.set) {
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.set,
received: ctx.parsedType
});
return INVALID;
}
const def = this._def;
if (def.minSize !== null) {
if (ctx.data.size < def.minSize.value) {
addIssueToContext(ctx, {
code: ZodIssueCode.too_small,
minimum: def.minSize.value,
type: "set",
inclusive: true,
exact: false,
message: def.minSize.message
});
status.dirty();
}
}
if (def.maxSize !== null) {
if (ctx.data.size > def.maxSize.value) {
addIssueToContext(ctx, {
code: ZodIssueCode.too_big,
maximum: def.maxSize.value,
type: "set",
inclusive: true,
exact: false,
message: def.maxSize.message
});
status.dirty();
}
}
const valueType = this._def.valueType;
function finalizeSet(elements2) {
const parsedSet = /* @__PURE__ */ new Set();
for (const element of elements2) {
if (element.status === "aborted")
return INVALID;
if (element.status === "dirty")
status.dirty();
parsedSet.add(element.value);
}
return { status: status.value, value: parsedSet };
}
const elements = [...ctx.data.values()].map((item, i) => valueType._parse(new ParseInputLazyPath(ctx, item, ctx.path, i)));
if (ctx.common.async) {
return Promise.all(elements).then((elements2) => finalizeSet(elements2));
} else {
return finalizeSet(elements);
}
}
min(minSize, message) {
return new ZodSet({
...this._def,
minSize: { value: minSize, message: errorUtil.toString(message) }
});
}
max(maxSize, message) {
return new ZodSet({
...this._def,
maxSize: { value: maxSize, message: errorUtil.toString(message) }
});
}
size(size, message) {
return this.min(size, message).max(size, message);
}
nonempty(message) {
return this.min(1, message);
}
}
ZodSet.create = (valueType, params) => {
return new ZodSet({
valueType,
minSize: null,
maxSize: null,
typeName: ZodFirstPartyTypeKind.ZodSet,
...processCreateParams(params)
});
};
class ZodLazy extends ZodType {
get schema() {
return this._def.getter();
}
_parse(input) {
const { ctx } = this._processInputParams(input);
const lazySchema = this._def.getter();
return lazySchema._parse({ data: ctx.data, path: ctx.path, parent: ctx });
}
}
ZodLazy.create = (getter, params) => {
return new ZodLazy({
getter,
typeName: ZodFirstPartyTypeKind.ZodLazy,
...processCreateParams(params)
});
};
class ZodLiteral extends ZodType {
_parse(input) {
if (input.data !== this._def.value) {
const ctx = this._getOrReturnCtx(input);
addIssueToContext(ctx, {
received: ctx.data,
code: ZodIssueCode.invalid_literal,
expected: this._def.value
});
return INVALID;
}
return { status: "valid", value: input.data };
}
get value() {
return this._def.value;
}
}
ZodLiteral.create = (value, params) => {
return new ZodLiteral({
value,
typeName: ZodFirstPartyTypeKind.ZodLiteral,
...processCreateParams(params)
});
};
function createZodEnum(values, params) {
return new ZodEnum({
values,
typeName: ZodFirstPartyTypeKind.ZodEnum,
...processCreateParams(params)
});
}
class ZodEnum extends ZodType {
_parse(input) {
if (typeof input.data !== "string") {
const ctx = this._getOrReturnCtx(input);
const expectedValues = this._def.values;
addIssueToContext(ctx, {
expected: util.joinValues(expectedValues),
received: ctx.parsedType,
code: ZodIssueCode.invalid_type
});
return INVALID;
}
if (!this._cache) {
this._cache = new Set(this._def.values);
}
if (!this._cache.has(input.data)) {
const ctx = this._getOrReturnCtx(input);
const expectedValues = this._def.values;
addIssueToContext(ctx, {
received: ctx.data,
code: ZodIssueCode.invalid_enum_value,
options: expectedValues
});
return INVALID;
}
return OK(input.data);
}
get options() {
return this._def.values;
}
get enum() {
const enumValues = {};
for (const val of this._def.values) {
enumValues[val] = val;
}
return enumValues;
}
get Values() {
const enumValues = {};
for (const val of this._def.values) {
enumValues[val] = val;
}
return enumValues;
}
get Enum() {
const enumValues = {};
for (const val of this._def.values) {
enumValues[val] = val;
}
return enumValues;
}
extract(values, newDef = this._def) {
return ZodEnum.create(values, {
...this._def,
...newDef
});
}
exclude(values, newDef = this._def) {
return ZodEnum.create(this.options.filter((opt) => !values.includes(opt)), {
...this._def,
...newDef
});
}
}
ZodEnum.create = createZodEnum;
class ZodNativeEnum extends ZodType {
_parse(input) {
const nativeEnumValues = util.getValidEnumValues(this._def.values);
const ctx = this._getOrReturnCtx(input);
if (ctx.parsedType !== ZodParsedType.string && ctx.parsedType !== ZodParsedType.number) {
const expectedValues = util.objectValues(nativeEnumValues);
addIssueToContext(ctx, {
expected: util.joinValues(expectedValues),
received: ctx.parsedType,
code: ZodIssueCode.invalid_type
});
return INVALID;
}
if (!this._cache) {
this._cache = new Set(util.getValidEnumValues(this._def.values));
}
if (!this._cache.has(input.data)) {
const expectedValues = util.objectValues(nativeEnumValues);
addIssueToContext(ctx, {
received: ctx.data,
code: ZodIssueCode.invalid_enum_value,
options: expectedValues
});
return INVALID;
}
return OK(input.data);
}
get enum() {
return this._def.values;
}
}
ZodNativeEnum.create = (values, params) => {
return new ZodNativeEnum({
values,
typeName: ZodFirstPartyTypeKind.ZodNativeEnum,
...processCreateParams(params)
});
};
class ZodPromise extends ZodType {
unwrap() {
return this._def.type;
}
_parse(input) {
const { ctx } = this._processInputParams(input);
if (ctx.parsedType !== ZodParsedType.promise && ctx.common.async === false) {
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.promise,
received: ctx.parsedType
});
return INVALID;
}
const promisified = ctx.parsedType === ZodParsedType.promise ? ctx.data : Promise.resolve(ctx.data);
return OK(promisified.then((data) => {
return this._def.type.parseAsync(data, {
path: ctx.path,
errorMap: ctx.common.contextualErrorMap
});
}));
}
}
ZodPromise.create = (schema, params) => {
return new ZodPromise({
type: schema,
typeName: ZodFirstPartyTypeKind.ZodPromise,
...processCreateParams(params)
});
};
class ZodEffects extends ZodType {
innerType() {
return this._def.schema;
}
sourceType() {
return this._def.schema._def.typeName === ZodFirstPartyTypeKind.ZodEffects ? this._def.schema.sourceType() : this._def.schema;
}
_parse(input) {
const { status, ctx } = this._processInputParams(input);
const effect = this._def.effect || null;
const checkCtx = {
addIssue: (arg) => {
addIssueToContext(ctx, arg);
if (arg.fatal) {
status.abort();
} else {
status.dirty();
}
},
get path() {
return ctx.path;
}
};
checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx);
if (effect.type === "preprocess") {
const processed = effect.transform(ctx.data, checkCtx);
if (ctx.common.async) {
return Promise.resolve(processed).then(async (processed2) => {
if (status.value === "aborted")
return INVALID;
const result = await this._def.schema._parseAsync({
data: processed2,
path: ctx.path,
parent: ctx
});
if (result.status === "aborted")
return INVALID;
if (result.status === "dirty")
return DIRTY(result.value);
if (status.value === "dirty")
return DIRTY(result.value);
return result;
});
} else {
if (status.value === "aborted")
return INVALID;
const result = this._def.schema._parseSync({
data: processed,
path: ctx.path,
parent: ctx
});
if (result.status === "aborted")
return INVALID;
if (result.status === "dirty")
return DIRTY(result.value);
if (status.value === "dirty")
return DIRTY(result.value);
return result;
}
}
if (effect.type === "refinement") {
const executeRefinement = (acc) => {
const result = effect.refinement(acc, checkCtx);
if (ctx.common.async) {
return Promise.resolve(result);
}
if (result instanceof Promise) {
throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead.");
}
return acc;
};
if (ctx.common.async === false) {
const inner = this._def.schema._parseSync({
data: ctx.data,
path: ctx.path,
parent: ctx
});
if (inner.status === "aborted")
return INVALID;
if (inner.status === "dirty")
status.dirty();
executeRefinement(inner.value);
return { status: status.value, value: inner.value };
} else {
return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((inner) => {
if (inner.status === "aborted")
return INVALID;
if (inner.status === "dirty")
status.dirty();
return executeRefinement(inner.value).then(() => {
return { status: status.value, value: inner.value };
});
});
}
}
if (effect.type === "transform") {
if (ctx.common.async === false) {
const base = this._def.schema._parseSync({
data: ctx.data,
path: ctx.path,
parent: ctx
});
if (!isValid(base))
return INVALID;
const result = effect.transform(base.value, checkCtx);
if (result instanceof Promise) {
throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`);
}
return { status: status.value, value: result };
} else {
return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => {
if (!isValid(base))
return INVALID;
return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({
status: status.value,
value: result
}));
});
}
}
util.assertNever(effect);
}
}
ZodEffects.create = (schema, effect, params) => {
return new ZodEffects({
schema,
typeName: ZodFirstPartyTypeKind.ZodEffects,
effect,
...processCreateParams(params)
});
};
ZodEffects.createWithPreprocess = (preprocess, schema, params) => {
return new ZodEffects({
schema,
effect: { type: "preprocess", transform: preprocess },
typeName: ZodFirstPartyTypeKind.ZodEffects,
...processCreateParams(params)
});
};
class ZodOptional extends ZodType {
_parse(input) {
const parsedType = this._getType(input);
if (parsedType === ZodParsedType.undefined) {
return OK(void 0);
}
return this._def.innerType._parse(input);
}
unwrap() {
return this._def.innerType;
}
}
ZodOptional.create = (type, params) => {
return new ZodOptional({
innerType: type,
typeName: ZodFirstPartyTypeKind.ZodOptional,
...processCreateParams(params)
});
};
class ZodNullable extends ZodType {
_parse(input) {
const parsedType = this._getType(input);
if (parsedType === ZodParsedType.null) {
return OK(null);
}
return this._def.innerType._parse(input);
}
unwrap() {
return this._def.innerType;
}
}
ZodNullable.create = (type, params) => {
return new ZodNullable({
innerType: type,
typeName: ZodFirstPartyTypeKind.ZodNullable,
...processCreateParams(params)
});
};
class ZodDefault extends ZodType {
_parse(input) {
const { ctx } = this._processInputParams(input);
let data = ctx.data;
if (ctx.parsedType === ZodParsedType.undefined) {
data = this._def.defaultValue();
}
return this._def.innerType._parse({
data,
path: ctx.path,
parent: ctx
});
}
removeDefault() {
return this._def.innerType;
}
}
ZodDefault.create = (type, params) => {
return new ZodDefault({
innerType: type,
typeName: ZodFirstPartyTypeKind.ZodDefault,
defaultValue: typeof params.default === "function" ? params.default : () => params.default,
...processCreateParams(params)
});
};
class ZodCatch extends ZodType {
_parse(input) {
const { ctx } = this._processInputParams(input);
const newCtx = {
...ctx,
common: {
...ctx.common,
issues: []
}
};
const result = this._def.innerType._parse({
data: newCtx.data,
path: newCtx.path,
parent: {
...newCtx
}
});
if (isAsync(result)) {
return result.then((result2) => {
return {
status: "valid",
value: result2.status === "valid" ? result2.value : this._def.catchValue({
get error() {
return new ZodError(newCtx.common.issues);
},
input: newCtx.data
})
};
});
} else {
return {
status: "valid",
value: result.status === "valid" ? result.value : this._def.catchValue({
get error() {
return new ZodError(newCtx.common.issues);
},
input: newCtx.data
})
};
}
}
removeCatch() {
return this._def.innerType;
}
}
ZodCatch.create = (type, params) => {
return new ZodCatch({
innerType: type,
typeName: ZodFirstPartyTypeKind.ZodCatch,
catchValue: typeof params.catch === "function" ? params.catch : () => params.catch,
...processCreateParams(params)
});
};
class ZodNaN extends ZodType {
_parse(input) {
const parsedType = this._getType(input);
if (parsedType !== ZodParsedType.nan) {
const ctx = this._getOrReturnCtx(input);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.nan,
received: ctx.parsedType
});
return INVALID;
}
return { status: "valid", value: input.data };
}
}
ZodNaN.create = (params) => {
return new ZodNaN({
typeName: ZodFirstPartyTypeKind.ZodNaN,
...processCreateParams(params)
});
};
class ZodBranded extends ZodType {
_parse(input) {
const { ctx } = this._processInputParams(input);
const data = ctx.data;
return this._def.type._parse({
data,
path: ctx.path,
parent: ctx
});
}
unwrap() {
return this._def.type;
}
}
class ZodPipeline extends ZodType {
_parse(input) {
const { status, ctx } = this._processInputParams(input);
if (ctx.common.async) {
const handleAsync = async () => {
const inResult = await this._def.in._parseAsync({
data: ctx.data,
path: ctx.path,
parent: ctx
});
if (inResult.status === "aborted")
return INVALID;
if (inResult.status === "dirty") {
status.dirty();
return DIRTY(inResult.value);
} else {
return this._def.out._parseAsync({
data: inResult.value,
path: ctx.path,
parent: ctx
});
}
};
return handleAsync();
} else {
const inResult = this._def.in._parseSync({
data: ctx.data,
path: ctx.path,
parent: ctx
});
if (inResult.status === "aborted")
return INVALID;
if (inResult.status === "dirty") {
status.dirty();
return {
status: "dirty",
value: inResult.value
};
} else {
return this._def.out._parseSync({
data: inResult.value,
path: ctx.path,
parent: ctx
});
}
}
}
static create(a, b) {
return new ZodPipeline({
in: a,
out: b,
typeName: ZodFirstPartyTypeKind.ZodPipeline
});
}
}
class ZodReadonly extends ZodType {
_parse(input) {
const result = this._def.innerType._parse(input);
const freeze = (data) => {
if (isValid(data)) {
data.value = Object.freeze(data.value);
}
return data;
};
return isAsync(result) ? result.then((data) => freeze(data)) : freeze(result);
}
unwrap() {
return this._def.innerType;
}
}
ZodReadonly.create = (type, params) => {
return new ZodReadonly({
innerType: type,
typeName: ZodFirstPartyTypeKind.ZodReadonly,
...processCreateParams(params)
});
};
var ZodFirstPartyTypeKind;
(function(ZodFirstPartyTypeKind2) {
ZodFirstPartyTypeKind2["ZodString"] = "ZodString";
ZodFirstPartyTypeKind2["ZodNumber"] = "ZodNumber";
ZodFirstPartyTypeKind2["ZodNaN"] = "ZodNaN";
ZodFirstPartyTypeKind2["ZodBigInt"] = "ZodBigInt";
ZodFirstPartyTypeKind2["ZodBoolean"] = "ZodBoolean";
ZodFirstPartyTypeKind2["ZodDate"] = "ZodDate";
ZodFirstPartyTypeKind2["ZodSymbol"] = "ZodSymbol";
ZodFirstPartyTypeKind2["ZodUndefined"] = "ZodUndefined";
ZodFirstPartyTypeKind2["ZodNull"] = "ZodNull";
ZodFirstPartyTypeKind2["ZodAny"] = "ZodAny";
ZodFirstPartyTypeKind2["ZodUnknown"] = "ZodUnknown";
ZodFirstPartyTypeKind2["ZodNever"] = "ZodNever";
ZodFirstPartyTypeKind2["ZodVoid"] = "ZodVoid";
ZodFirstPartyTypeKind2["ZodArray"] = "ZodArray";
ZodFirstPartyTypeKind2["ZodObject"] = "ZodObject";
ZodFirstPartyTypeKind2["ZodUnion"] = "ZodUnion";
ZodFirstPartyTypeKind2["ZodDiscriminatedUnion"] = "ZodDiscriminatedUnion";
ZodFirstPartyTypeKind2["ZodIntersection"] = "ZodIntersection";
ZodFirstPartyTypeKind2["ZodTuple"] = "ZodTuple";
ZodFirstPartyTypeKind2["ZodRecord"] = "ZodRecord";
ZodFirstPartyTypeKind2["ZodMap"] = "ZodMap";
ZodFirstPartyTypeKind2["ZodSet"] = "ZodSet";
ZodFirstPartyTypeKind2["ZodFunction"] = "ZodFunction";
ZodFirstPartyTypeKind2["ZodLazy"] = "ZodLazy";
ZodFirstPartyTypeKind2["ZodLiteral"] = "ZodLiteral";
ZodFirstPartyTypeKind2["ZodEnum"] = "ZodEnum";
ZodFirstPartyTypeKind2["ZodEffects"] = "ZodEffects";
ZodFirstPartyTypeKind2["ZodNativeEnum"] = "ZodNativeEnum";
ZodFirstPartyTypeKind2["ZodOptional"] = "ZodOptional";
ZodFirstPartyTypeKind2["ZodNullable"] = "ZodNullable";
ZodFirstPartyTypeKind2["ZodDefault"] = "ZodDefault";
ZodFirstPartyTypeKind2["ZodCatch"] = "ZodCatch";
ZodFirstPartyTypeKind2["ZodPromise"] = "ZodPromise";
ZodFirstPartyTypeKind2["ZodBranded"] = "ZodBranded";
ZodFirstPartyTypeKind2["ZodPipeline"] = "ZodPipeline";
ZodFirstPartyTypeKind2["ZodReadonly"] = "ZodReadonly";
})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {}));
const stringType = ZodString.create;
const numberType = ZodNumber.create;
const booleanType = ZodBoolean.create;
const nullType = ZodNull.create;
ZodNever.create;
const arrayType = ZodArray.create;
const objectType = ZodObject.create;
const unionType = ZodUnion.create;
const discriminatedUnionType = ZodDiscriminatedUnion.create;
ZodIntersection.create;
ZodTuple.create;
const recordType = ZodRecord.create;
const lazyType = ZodLazy.create;
const literalType = ZodLiteral.create;
const enumType = ZodEnum.create;
const nativeEnumType = ZodNativeEnum.create;
ZodPromise.create;
ZodOptional.create;
ZodNullable.create;
const lazyOnce = (getter) => {
let cached;
return lazyType(() => {
cached ?? (cached = getter());
return cached;
});
};
const QUESTION_TREE_LIMITS = {
maxRootBytes: 512 * 1024,
maxDepth: 12,
maxNodes: 512,
maxCollectionSize: 256,
maxContentBytes: 64 * 1024
};
const utf8Length$4 = (value) => new TextEncoder().encode(value).length;
const ContentSchema = stringType().refine(
(value) => utf8Length$4(value) <= QUESTION_TREE_LIMITS.maxContentBytes,
"question content exceeds byte limit"
);
const NonEmptyContentSchema = ContentSchema.refine(
(value) => value.trim().length > 0,
"question content must not be empty"
);
const StableIdSchema$1 = stringType().min(1).max(256).refine(
(value) => [...value].every((character) => {
const code = character.charCodeAt(0);
return code > 31 && code !== 127;
}),
"invalid id"
);
const NodePathSchema = stringType().min(2).max(4096).regex(/^\/(?:[^/~]|~[01])+(?:\/(?:[^/~]|~[01])+)*$/, "invalid path");
const QuestionOptionSchema = objectType({
id: StableIdSchema$1,
content: NonEmptyContentSchema
}).strict();
const AnswerSlotSchema = objectType({
id: StableIdSchema$1,
label: ContentSchema.optional(),
options: arrayType(QuestionOptionSchema).max(QUESTION_TREE_LIMITS.maxCollectionSize).optional()
}).strict().superRefine((slot, ctx) => {
var _a2;
if (slot.options && slot.options.length === 0) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["options"],
message: "slot options must not be empty"
});
}
addDuplicateIdIssue(slot.options ?? [], ctx, ["options"]);
if (slot.options && !((_a2 = slot.label) == null ? void 0 : _a2.trim())) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["label"],
message: "dropdown slot requires a label"
});
}
});
const QuestionMetadataSchema = objectType({
platform: stringType().max(128).optional(),
variant: stringType().max(128).optional(),
ruleVersion: stringType().max(128).optional(),
sourceType: stringType().max(128).optional(),
score: numberType().finite().optional()
}).strict();
function addDuplicateIdIssue(values, ctx, path) {
var _a2;
const seen = /* @__PURE__ */ new Set();
for (let index = 0; index < values.length; index += 1) {
const id = (_a2 = values[index]) == null ? void 0 : _a2.id;
if (id == null) continue;
if (seen.has(id)) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: [...path, index, "id"],
message: `duplicate id: ${id}`
});
}
seen.add(id);
}
}
const LeafQuestionNodeSchema = objectType({
kind: literalType("leaf"),
id: StableIdSchema$1,
path: NodePathSchema,
type: enumType(["single", "multiple", "judge", "fill", "short_answer"]),
stem: NonEmptyContentSchema,
options: arrayType(QuestionOptionSchema).max(QUESTION_TREE_LIMITS.maxCollectionSize),
slots: arrayType(AnswerSlotSchema).max(QUESTION_TREE_LIMITS.maxCollectionSize),
fillPolicy: enumType(["atomic", "per-slot"]),
metadata: QuestionMetadataSchema.optional()
}).strict().superRefine((node, ctx) => {
addDuplicateIdIssue(node.options, ctx, ["options"]);
addDuplicateIdIssue(node.slots, ctx, ["slots"]);
const isChoice = ["single", "multiple", "judge"].includes(node.type);
if (isChoice && (node.options.length === 0 || node.slots.length !== 0)) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: "choice questions require options and forbid slots"
});
}
if (!isChoice && (node.options.length !== 0 || node.slots.length === 0)) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: "text questions require slots and forbid root options"
});
}
const dropdownSlots = node.slots.filter((slot) => slot.options != null);
if (dropdownSlots.length > 0 && node.slots.length !== 1) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["slots"],
message: "each dropdown must be represented by one leaf slot"
});
}
});
const MatchingQuestionNodeSchema = objectType({
kind: literalType("matching"),
id: StableIdSchema$1,
path: NodePathSchema,
type: literalType("matching"),
stem: NonEmptyContentSchema,
left: arrayType(QuestionOptionSchema).min(1).max(QUESTION_TREE_LIMITS.maxCollectionSize),
right: arrayType(QuestionOptionSchema).min(1).max(QUESTION_TREE_LIMITS.maxCollectionSize),
cardinality: enumType(["one-to-one", "many-to-one"]),
fillPolicy: enumType(["atomic", "per-pair"]),
metadata: QuestionMetadataSchema.optional()
}).strict().superRefine((node, ctx) => {
addDuplicateIdIssue(node.left, ctx, ["left"]);
addDuplicateIdIssue(node.right, ctx, ["right"]);
});
const CompositeQuestionNodeSchema = objectType({
kind: literalType("composite"),
id: StableIdSchema$1,
path: NodePathSchema,
type: enumType(["composite", "cloze", "reading", "analysis"]),
stem: ContentSchema,
children: arrayType(lazyType(() => QuestionNodeSchema)).min(1).max(QUESTION_TREE_LIMITS.maxCollectionSize),
fillPolicy: enumType(["atomic", "per-child"]),
metadata: QuestionMetadataSchema.optional()
}).strict().superRefine((node, ctx) => {
addDuplicateIdIssue(node.children, ctx, ["children"]);
});
const QuestionNodeSchema = lazyOnce(
() => unionType([
LeafQuestionNodeSchema,
CompositeQuestionNodeSchema,
MatchingQuestionNodeSchema
])
);
function preflightTree(root) {
let nodes = 0;
const active2 = /* @__PURE__ */ new WeakSet();
const visit = (value, depth) => {
if (depth > QUESTION_TREE_LIMITS.maxDepth) {
throw new Error("question tree exceeds max depth");
}
if (typeof value !== "object" || value === null) return;
if (active2.has(value)) throw new Error("question tree contains a cycle");
active2.add(value);
nodes += 1;
if (nodes > QUESTION_TREE_LIMITS.maxNodes) {
throw new Error("question tree exceeds max node count");
}
const candidate = value;
if (candidate.kind === "composite" && Array.isArray(candidate.children)) {
for (const child of candidate.children) visit(child, depth + 1);
}
active2.delete(value);
};
visit(root, 1);
let encoded;
try {
encoded = JSON.stringify(root);
} catch {
throw new Error("question tree is not serializable");
}
if (utf8Length$4(encoded) > QUESTION_TREE_LIMITS.maxRootBytes) {
throw new Error("question tree exceeds max byte size");
}
}
function escapeNodePathSegment(value) {
return value.replace(/~/g, "~0").replace(/\//g, "~1");
}
function deriveQuestionPaths(root) {
const visit = (node, path) => {
if (node.kind !== "composite") return { ...node, path };
return {
...node,
path,
children: node.children.map(
(child) => visit(child, `${path}/children/${escapeNodePathSegment(child.id)}`)
)
};
};
return visit(root, `/${escapeNodePathSegment(root.id)}`);
}
function assertValidQuestionTree(root) {
preflightTree(root);
const parsed = QuestionNodeSchema.parse(root);
const derived = deriveQuestionPaths(parsed);
const compare = (actual, expected) => {
if (actual.path !== expected.path) {
throw new Error(
`question path mismatch for ${actual.id}: ${actual.path} !== ${expected.path}`
);
}
if (actual.kind === "composite" && expected.kind === "composite") {
for (let index = 0; index < actual.children.length; index += 1) {
const actualChild = actual.children[index];
const expectedChild = expected.children[index];
if (actualChild && expectedChild) compare(actualChild, expectedChild);
}
}
};
compare(parsed, derived);
}
function formatSearchStem(segments) {
return segments.map((segment) => segment.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
}
function createUnit(input) {
return {
rootHash: input.rootHash,
unitPath: input.unitPath,
sourceNodeHash: input.sourceNodeHash,
unitHash: searchUnitHash(input),
queryType: input.queryType,
effectiveStem: formatSearchStem(input.stemSegments),
options: input.options,
answerShape: input.answerShape
};
}
function flattenLeaf(node, rootHash, contexts) {
const sourceNodeHash = questionNodeHash(node);
const slot = node.slots[0];
const isDropdown = (slot == null ? void 0 : slot.options) != null;
const queryType = isDropdown ? "single" : node.type;
const options = isDropdown ? slot.options ?? [] : node.options;
const answerShape = [
"single",
"multiple",
"judge"
].includes(node.type) ? {
kind: "choice",
min: 1,
max: node.type === "multiple" ? node.options.length : 1
} : { kind: "slots", slotIds: node.slots.map((item) => item.id) };
const stemSegments = [
...contexts,
node.stem,
...isDropdown && (slot == null ? void 0 : slot.label) ? [slot.label] : []
];
return createUnit({
rootHash,
unitPath: node.path,
sourceNodeHash,
queryType,
stemSegments,
options,
answerShape
});
}
function flattenMatching(node, rootHash, contexts) {
const sourceNodeHash = questionNodeHash(node);
return node.left.map((left) => {
const answerShape = {
kind: "matching-pair",
leftId: left.id,
rightIds: node.right.map((right) => right.id)
};
return createUnit({
rootHash,
unitPath: `${node.path}/pairs/${escapeNodePathSegment(left.id)}`,
sourceNodeHash,
queryType: "single",
stemSegments: [...contexts, node.stem, left.content],
options: node.right,
answerShape
});
});
}
function flattenQuestionTree(root) {
assertValidQuestionTree(root);
const rootHash = questionNodeHash(root);
const units = [];
const visit = (node, contexts) => {
switch (node.kind) {
case "leaf":
units.push(flattenLeaf(node, rootHash, contexts));
break;
case "matching":
units.push(...flattenMatching(node, rootHash, contexts));
break;
case "composite": {
const nextContexts = node.stem.trim() ? [...contexts, node.stem] : contexts;
for (const child of node.children) visit(child, nextContexts);
break;
}
}
};
visit(root, []);
return units;
}
const successful = (answer) => answer.kind === "leaf" ? answer.status === "hit" : answer.status === "complete";
const missed = (answer) => answer.kind === "leaf" ? answer.status === "miss" : answer.status === "miss";
function aggregateStatus(children, atomic) {
if (children.every(successful)) return "complete";
if (children.every(missed)) return "miss";
const unsafe = children.some((child) => child.status === "unsafe");
return atomic && unsafe ? "unsafe" : "partial";
}
function assembleMatching(node, answerByPath) {
const pairs = node.left.map((left) => {
var _a2;
const result = answerByPath.get(
`${node.path}/pairs/${left.id.replace(/~/g, "~0").replace(/\//g, "~1")}`
);
const payload = ((_a2 = result == null ? void 0 : result.answer) == null ? void 0 : _a2.kind) === "matching-pair" ? result.answer : null;
return {
leftId: left.id,
...payload ? { rightId: payload.rightId, displayValue: payload.displayValue } : {},
status: (result == null ? void 0 : result.status) ?? "miss",
charged: (result == null ? void 0 : result.charged) ?? false
};
});
const status = pairs.every(
(pair) => pair.status === "hit"
) ? "complete" : pairs.every((pair) => pair.status === "miss") ? "miss" : node.fillPolicy === "atomic" && pairs.some((pair) => pair.status === "unsafe") ? "unsafe" : "partial";
return { kind: "matching", path: node.path, status, pairs };
}
function assembleAnswerTree(root, answers) {
const answerByPath = new Map(answers.map((answer) => [answer.path, answer]));
const unitByPath = new Map(
flattenQuestionTree(root).map((unit) => [unit.unitPath, unit])
);
const visit = (node) => {
var _a2;
if (node.kind === "leaf") {
return answerByPath.get(node.path) ?? {
kind: "leaf",
path: node.path,
unitHash: ((_a2 = unitByPath.get(node.path)) == null ? void 0 : _a2.unitHash) ?? "",
status: "miss",
answer: null,
charged: false
};
}
if (node.kind === "matching") {
return assembleMatching(node, answerByPath);
}
const children = node.children.map(visit);
return {
kind: "composite",
path: node.path,
status: aggregateStatus(children, node.fillPolicy === "atomic"),
children
};
};
return visit(root);
}
const HashV2Schema$1 = stringType().regex(/^[0-9a-f]{64}$/);
const LeafAnswerPayloadSchema = unionType([
objectType({
kind: literalType("choice"),
optionIds: arrayType(stringType().min(1)).min(1),
displayValues: arrayType(stringType()).min(1)
}).strict().refine((value) => value.optionIds.length === value.displayValues.length, {
message: "choice ids and display values must align"
}),
objectType({
kind: literalType("slots"),
slots: arrayType(
objectType({
slotId: stringType().min(1),
values: arrayType(stringType().min(1)).min(1)
}).strict()
).min(1)
}).strict()
]);
const MatchingPairPayloadSchema = objectType({
kind: literalType("matching-pair"),
leftId: stringType().min(1),
rightId: stringType().min(1),
displayValue: stringType().min(1)
}).strict();
const LeafAnswerNodeSchema = objectType({
kind: literalType("leaf"),
path: NodePathSchema,
unitHash: HashV2Schema$1,
status: enumType([
"hit",
"miss",
"busy",
"unauthorized",
"insufficient",
"rate_limited",
"invalid",
"unsafe"
]),
answer: unionType([LeafAnswerPayloadSchema, MatchingPairPayloadSchema]).nullable(),
source: enumType(["free", "cache", "relay", "local"]).optional(),
aiGenerated: booleanType().optional(),
charged: booleanType()
}).strict().superRefine((node, ctx) => {
if (node.status === "hit" && node.answer == null) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["answer"],
message: "hit requires a typed answer"
});
}
if (node.status !== "hit" && node.answer != null) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["answer"],
message: "non-hit must not expose answer candidates"
});
}
if (node.charged && node.status !== "hit") {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["charged"],
message: "only usable hits can be charged"
});
}
});
const CompositeAnswerNodeSchema = lazyOnce(
() => objectType({
kind: literalType("composite"),
path: NodePathSchema,
status: enumType(["complete", "partial", "miss", "unsafe"]),
children: arrayType(AnswerNodeSchema).min(1)
}).strict()
);
const MatchingAnswerNodeSchema = objectType({
kind: literalType("matching"),
path: NodePathSchema,
status: enumType(["complete", "partial", "miss", "unsafe"]),
pairs: arrayType(
objectType({
leftId: stringType().min(1),
rightId: stringType().min(1).optional(),
displayValue: stringType().min(1).optional(),
status: enumType([
"hit",
"miss",
"busy",
"unauthorized",
"insufficient",
"rate_limited",
"invalid",
"unsafe"
]),
charged: booleanType()
}).strict()
)
}).strict();
const AnswerNodeSchema = lazyOnce(
() => unionType([
LeafAnswerNodeSchema,
CompositeAnswerNodeSchema,
MatchingAnswerNodeSchema
])
);
const SEARCH_PATH = "/api/search";
const HashV2Schema = stringType().regex(/^[0-9a-f]{64}$/);
objectType({
requestSchemaVersion: literalType(2),
root: QuestionNodeSchema,
unitPath: NodePathSchema,
expectedRootHash: HashV2Schema.optional(),
expectedUnitHash: HashV2Schema.optional()
}).strict().superRefine((request, ctx) => {
try {
assertValidQuestionTree(request.root);
} catch (error) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["root"],
message: error instanceof Error ? error.message : "invalid question tree"
});
}
});
const SearchUnitResponseSchema = objectType({
code: nativeEnumType(AiAskCode),
found: booleanType(),
result: LeafAnswerNodeSchema.nullable().default(null)
}).strict().superRefine((response, ctx) => {
var _a2;
const isHit = ((_a2 = response.result) == null ? void 0 : _a2.status) === "hit";
if (response.found !== isHit) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["result"],
message: "found must match typed hit status"
});
}
if (response.found && response.code !== AiAskCode.Ok) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["code"],
message: "found response requires Ok code"
});
}
});
const AUTH_REGISTER_PATH = "/api/auth/register";
const AUTH_LOGIN_PATH = "/api/auth/login";
const USERNAME_MAX = 32;
const PASSWORD_MAX = 128;
const AuthCredentialsSchema = objectType({
username: stringType().min(3).max(USERNAME_MAX),
password: stringType().min(8).max(PASSWORD_MAX)
});
objectType({
username: stringType().min(1).max(USERNAME_MAX),
password: stringType().min(1).max(PASSWORD_MAX)
});
AuthCredentialsSchema.extend({
captchaToken: stringType().min(1).max(4096),
/**
* 选填。**只用于找回密码**,不作登录标识、不回显、不参与鉴权。
*
* 不填的账号没有找回通道(S4 spec §5)——这是明确接受的代价,
* 面板与网页两处都要在填写处把这句话写出来,不能让人事后才发现。
*
* 客户端「没填」必须表现为**键不存在**,不是空串:空串过不了
* `.email()`,整笔注册会被判 Invalid 而失败。
*/
email: stringType().email().max(128).optional()
});
const AuthResponseSchema = objectType({
code: nativeEnumType(AiAskCode),
token: stringType().optional(),
reason: enumType(["taken", "disabled"]).optional()
});
const PASSWORD_RESET_CODE_LENGTH = 6;
objectType({
identifier: stringType().min(1).max(128),
captchaToken: stringType().min(1).max(4096)
});
objectType({
identifier: stringType().min(1).max(128),
code: stringType().length(PASSWORD_RESET_CODE_LENGTH),
/** 与注册同一条口径(`AuthCredentialsSchema.password`):8–128。 */
newPassword: stringType().min(8).max(128)
});
objectType({
code: nativeEnumType(AiAskCode),
reason: enumType(["sent", "disabled", "invalid_code"]).optional()
});
const REDEEM_PATH = "/api/redeem";
objectType({
code: stringType().min(1).max(128)
});
const RedeemResponseSchema = objectType({
code: nativeEnumType(AiAskCode),
balance: numberType().optional()
});
const ME_PATH = "/api/me";
objectType({}).strict();
const MeResponseSchema = objectType({
code: nativeEnumType(AiAskCode),
username: stringType().optional(),
balance: numberType().int().optional(),
/**
* 有没有绑过邮箱。**只回布尔,不回邮箱本身**——这个端点的调用方是油猴
* 面板与网页,回真值等于把邮箱铺到每个装了脚本的页面上,而界面上要回答
* 的问题只有一个:「我忘了密码还能不能找回」。
*
* 注册时不填邮箱的账号没有找回通道(S4 spec §5),这是明确接受的代价;
* 但用户得先知道自己属于哪一档,否则只有在真忘了密码那天才发现。
*/
emailBound: booleanType().optional()
}).strict();
objectType({
limit: numberType().int().min(1).max(50).default(20),
cursor: stringType().regex(/^\d+$/u).max(20).optional()
}).strict();
const MeLedgerItemSchema = objectType({
id: stringType(),
at: stringType().datetime(),
/** `search` | `ai_fallback` | `daily_grant` …,客户端按未知值兜底显示原文。 */
scene: stringType(),
/** `charged` | `refunded` | `free` | `reserved`。 */
state: stringType(),
amount: numberType().int()
}).strict();
objectType({
code: nativeEnumType(AiAskCode),
/** 这个账号有没有老线密钥。老库导入来的账号才有,纯新注册的没有。 */
present: booleanType().optional()
}).strict();
objectType({
code: nativeEnumType(AiAskCode),
/**
* 新钥匙的**明文,只在这一次响应里出现**——服务端只存 HMAC 哈希,
* 关掉页面就再也拿不回来(要么再轮换一次)。
*
* 明文出 wire 是有意的:用户必须拿到它去更新老脚本,不给他就等于把他的
* 脚本打死。它走的是与账号 token 同一条安全信封,暴露面不比登录响应更大。
*/
apiKey: stringType().optional()
}).strict();
objectType({
code: nativeEnumType(AiAskCode),
items: arrayType(MeLedgerItemSchema).max(50),
nextCursor: stringType().nullable()
}).strict();
objectType({ email: stringType().email().max(128) }).strict();
objectType({
code: nativeEnumType(AiAskCode),
reason: enumType(["sent", "cooldown", "taken", "already_bound"]).optional()
}).strict();
objectType({
email: stringType().email().max(128),
code: stringType().length(PASSWORD_RESET_CODE_LENGTH)
}).strict();
objectType({
code: nativeEnumType(AiAskCode),
reason: enumType(["bound", "invalid_code", "taken", "already_bound"]).optional()
}).strict();
objectType({
currentPassword: stringType().min(1).max(128),
newPassword: stringType().min(8).max(128)
}).strict();
objectType({
code: nativeEnumType(AiAskCode),
reason: enumType(["changed", "wrong_password"]).optional()
}).strict();
const RULE_HARD_LIMITS = {
maxPackageBytes: 512 * 1024,
maxSteps: 5e4,
maxWallMs: 1e4,
maxAsyncMs: 8e3,
maxLoopIterations: 1e3,
maxCallDepth: 32,
maxDomRefs: 5e3,
maxRegexPatternBytes: 2 * 1024,
maxRegexValueBytes: 128 * 1024
};
const FORBIDDEN_KEYS$3 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
const utf8Length$3 = (value) => new TextEncoder().encode(value).length;
const SafeObjectKeySchema = stringType().min(1).max(256).refine((value) => !FORBIDDEN_KEYS$3.has(value), "forbidden object key");
const DeclaredVariableSchema = stringType().min(1).max(128).regex(/^[A-Za-z_][A-Za-z0-9_]*$/u, "invalid variable name").refine((value) => !FORBIDDEN_KEYS$3.has(value), "forbidden variable name");
const ReadVariableSchema = stringType().min(1).max(128).regex(/^\$?[A-Za-z_][A-Za-z0-9_]*$/u, "invalid variable name").refine(
(value) => !FORBIDDEN_KEYS$3.has(value.replace(/^\$/u, "")),
"forbidden variable name"
);
const StableIdSchema = stringType().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/u, "invalid stable id");
const VersionSchema = stringType().min(1).max(64).regex(
/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u,
"invalid version"
);
const base64Url$1 = (length) => stringType().length(length).regex(/^[A-Za-z0-9_-]+$/u, "invalid base64url");
const RuleContentHashSchema = base64Url$1(43);
const JsonObjectSchema = lazyOnce(
() => recordType(JsonRuleValueSchema).superRefine((value, ctx) => {
for (const key of Object.keys(value)) {
if (!SafeObjectKeySchema.safeParse(key).success) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: [key],
message: "forbidden object key"
});
}
}
})
);
const JsonRuleValueSchema = lazyOnce(
() => unionType([
nullType(),
booleanType(),
numberType().finite(),
stringType(),
arrayType(JsonRuleValueSchema).max(RULE_HARD_LIMITS.maxSteps),
JsonObjectSchema
])
);
const ExprRecordSchema = () => recordType(SafeObjectKeySchema, ExprSchema);
const RegexFlagsSchema = stringType().max(8).regex(/^[dgimsuvy]*$/u, "invalid regex flags").refine(
(value) => new Set(value).size === value.length,
"duplicate regex flag"
);
const ExprSchema = lazyOnce(
() => discriminatedUnionType("op", [
objectType({ op: literalType("literal"), value: JsonRuleValueSchema }).strict(),
objectType({ op: literalType("var"), name: ReadVariableSchema }).strict(),
objectType({
op: literalType("path"),
from: ExprSchema,
path: arrayType(unionType([SafeObjectKeySchema, numberType().int().nonnegative()])).max(256)
}).strict(),
objectType({
op: literalType("coalesce"),
values: arrayType(ExprSchema).min(1).max(256)
}).strict(),
objectType({
op: literalType("compare"),
kind: enumType(["eq", "ne", "gt", "gte", "lt", "lte"]),
left: ExprSchema,
right: ExprSchema
}).strict(),
objectType({
op: literalType("logic"),
kind: enumType(["and", "or"]),
values: arrayType(ExprSchema).min(1).max(256)
}).strict(),
objectType({ op: literalType("not"), value: ExprSchema }).strict(),
objectType({
op: literalType("array"),
items: arrayType(ExprSchema).max(RULE_HARD_LIMITS.maxSteps)
}).strict(),
objectType({
op: literalType("object"),
entries: ExprRecordSchema()
}).strict(),
objectType({
op: literalType("map"),
items: ExprSchema,
item: DeclaredVariableSchema,
index: DeclaredVariableSchema.optional(),
value: ExprSchema,
maxIterations: numberType().int().positive().max(RULE_HARD_LIMITS.maxLoopIterations)
}).strict(),
objectType({
op: literalType("filter"),
items: ExprSchema,
item: DeclaredVariableSchema,
index: DeclaredVariableSchema.optional(),
when: ExprSchema,
maxIterations: numberType().int().positive().max(RULE_HARD_LIMITS.maxLoopIterations)
}).strict(),
objectType({
op: literalType("reduce"),
items: ExprSchema,
item: DeclaredVariableSchema,
index: DeclaredVariableSchema.optional(),
accumulator: DeclaredVariableSchema,
initial: ExprSchema,
value: ExprSchema,
maxIterations: numberType().int().positive().max(RULE_HARD_LIMITS.maxLoopIterations)
}).strict(),
objectType({
op: literalType("string"),
kind: enumType(["trim", "collapseWs", "lower", "upper"]),
value: ExprSchema
}).strict(),
objectType({
op: literalType("regex"),
kind: enumType(["test", "extract", "replace"]),
value: ExprSchema,
pattern: stringType().refine(
(value) => utf8Length$3(value) <= RULE_HARD_LIMITS.maxRegexPatternBytes,
"regex pattern exceeds byte limit"
),
flags: RegexFlagsSchema.optional(),
replacement: stringType().refine(
(value) => utf8Length$3(value) <= RULE_HARD_LIMITS.maxRegexValueBytes,
"regex replacement exceeds byte limit"
).optional()
}).strict(),
objectType({
op: literalType("jsonPath"),
from: ExprSchema,
query: stringType().min(1).max(4096)
}).strict(),
objectType({
op: literalType("format"),
template: stringType().max(64 * 1024),
args: ExprRecordSchema()
}).strict()
])
);
const StepListSchema = () => arrayType(StepSchema).max(RULE_HARD_LIMITS.maxSteps);
const StepSchema = lazyOnce(
() => unionType([
objectType({
type: literalType("set"),
name: DeclaredVariableSchema,
value: ExprSchema
}).strict(),
objectType({
type: literalType("if"),
when: ExprSchema,
// biome-ignore lint/suspicious/noThenProperty: `then` 是冻结的 JSON DSL 字段名,不具备 Promise 语义。
then: StepListSchema(),
else: StepListSchema().optional()
}).strict(),
objectType({
type: literalType("switch"),
value: ExprSchema,
cases: arrayType(
objectType({
equals: JsonRuleValueSchema,
steps: StepListSchema()
}).strict()
).min(1).max(256),
default: StepListSchema().optional()
}).strict(),
objectType({
type: literalType("forEach"),
items: ExprSchema,
item: DeclaredVariableSchema,
index: DeclaredVariableSchema.optional(),
steps: StepListSchema(),
maxIterations: numberType().int().positive().max(RULE_HARD_LIMITS.maxLoopIterations)
}).strict(),
objectType({
type: literalType("while"),
when: ExprSchema,
steps: StepListSchema(),
maxIterations: numberType().int().positive().max(RULE_HARD_LIMITS.maxLoopIterations)
}).strict(),
objectType({
type: literalType("callFlow"),
flowId: StableIdSchema,
args: ExprRecordSchema().optional(),
result: DeclaredVariableSchema.optional()
}).strict(),
objectType({ type: literalType("return"), value: ExprSchema.optional() }).strict(),
objectType({
type: literalType("try"),
steps: StepListSchema(),
catch: StepListSchema().optional(),
finally: StepListSchema().optional()
}).strict().refine((step) => step.catch != null || step.finally != null, {
message: "try requires catch or finally"
}),
objectType({
type: literalType("primitive"),
id: stringType().min(3).max(128).regex(
/^[A-Za-z][A-Za-z0-9]*(?:\.[A-Za-z][A-Za-z0-9]*)+$/u,
"invalid primitive id"
),
args: ExprRecordSchema().optional(),
result: DeclaredVariableSchema.optional(),
timeoutMs: numberType().int().positive().max(RULE_HARD_LIMITS.maxAsyncMs).optional()
}).strict()
])
);
const FlowDefinitionSchema = objectType({
id: StableIdSchema,
params: arrayType(DeclaredVariableSchema).max(256).refine((values) => new Set(values).size === values.length, {
message: "duplicate flow param"
}).optional(),
steps: StepListSchema()
}).strict();
const RuleEventSchema = enumType([
"url-change",
"dom-change",
"frame-ready",
"api-captured",
"user-start",
"session-complete",
"timeout"
]);
const StateMachineDefinitionSchema = objectType({
initial: StableIdSchema,
states: recordType(
StableIdSchema,
objectType({
enter: StepListSchema().optional(),
transitions: arrayType(
objectType({
event: RuleEventSchema,
when: ExprSchema.optional(),
target: StableIdSchema,
actions: StepListSchema().optional()
}).strict()
).max(256)
}).strict()
)
}).strict().superRefine((machine, ctx) => {
if (!(machine.initial in machine.states)) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["initial"],
message: "initial state does not exist"
});
}
for (const [stateId, state] of Object.entries(machine.states)) {
for (let index = 0; index < state.transitions.length; index += 1) {
const transition = state.transitions[index];
if (transition && !(transition.target in machine.states)) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["states", stateId, "transitions", index, "target"],
message: "transition target does not exist"
});
}
}
}
});
const RuleCapabilitySchema = enumType([
"dom-read",
"frame-read",
"runtime-read",
"network-read",
"dom-mutate",
"ui-reveal",
"answer-write"
]);
const RuleLimitsSchema = objectType({
maxSteps: numberType().int().positive().max(RULE_HARD_LIMITS.maxSteps),
maxWallMs: numberType().int().positive().max(RULE_HARD_LIMITS.maxWallMs),
maxAsyncMs: numberType().int().positive().max(RULE_HARD_LIMITS.maxAsyncMs),
maxLoopIterations: numberType().int().positive().max(RULE_HARD_LIMITS.maxLoopIterations),
maxCallDepth: numberType().int().positive().max(RULE_HARD_LIMITS.maxCallDepth),
maxDomRefs: numberType().int().positive().max(RULE_HARD_LIMITS.maxDomRefs)
}).strict();
const PageVariantRuleSchema = objectType({
id: StableIdSchema,
title: stringType().min(1).max(256),
priority: numberType().int().min(-1e6).max(1e6),
match: FlowDefinitionSchema,
lifecycle: StateMachineDefinitionSchema.optional(),
capture: FlowDefinitionSchema,
fill: FlowDefinitionSchema,
diagnostics: FlowDefinitionSchema.optional(),
limits: RuleLimitsSchema.partial().optional()
}).strict().superRefine((variant, ctx) => {
var _a2;
const ids = [
variant.match.id,
variant.capture.id,
variant.fill.id,
(_a2 = variant.diagnostics) == null ? void 0 : _a2.id
].filter((id) => id != null);
if (new Set(ids).size !== ids.length) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: "duplicate flow id"
});
}
});
const RuleRollbackAuthorizationSchema = objectType({
toVersion: VersionSchema,
authorizationId: StableIdSchema
}).strict();
const ShellSelectorValueSchema = unionType([
stringType().min(1).max(256),
arrayType(stringType().min(1).max(256)).min(1).max(32)
]);
const RuleShellConfigSchema = objectType({
selectors: recordType(
stringType().regex(/^[a-z][a-zA-Z0-9.-]{0,47}$/u),
ShellSelectorValueSchema
).refine((value) => Object.keys(value).length <= 64, {
message: "shellConfig.selectors accepts at most 64 entries"
}).optional()
}).strict();
const RulePackageSchema = objectType({
schemaVersion: literalType(1),
packageId: StableIdSchema,
platform: StableIdSchema,
version: VersionSchema,
releaseSequence: numberType().int().nonnegative(),
engineRange: objectType({
min: VersionSchema,
maxExclusive: VersionSchema.optional()
}).strict(),
issuedAt: numberType().int().nonnegative(),
expiresAt: numberType().int().nonnegative().optional(),
signingKid: StableIdSchema,
rollbackAuthorization: RuleRollbackAuthorizationSchema.optional(),
capabilities: arrayType(RuleCapabilitySchema).max(16),
shellConfig: RuleShellConfigSchema.optional(),
variants: arrayType(PageVariantRuleSchema).min(1).max(256),
changelog: stringType().max(64 * 1024),
contentHash: RuleContentHashSchema,
signature: base64Url$1(86)
}).strict().superRefine((pkg, ctx) => {
if (pkg.expiresAt != null && pkg.expiresAt <= pkg.issuedAt) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["expiresAt"],
message: "package expiry must be after issue time"
});
}
if (new Set(pkg.capabilities).size !== pkg.capabilities.length) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["capabilities"],
message: "duplicate capability"
});
}
if (pkg.rollbackAuthorization != null && pkg.rollbackAuthorization.toVersion !== pkg.version) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["rollbackAuthorization", "toVersion"],
message: "rollback authorization must target package version"
});
}
const variantIds = pkg.variants.map((variant) => variant.id);
if (new Set(variantIds).size !== variantIds.length) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["variants"],
message: "duplicate variant id"
});
}
if (utf8Length$3(JSON.stringify(pkg)) > RULE_HARD_LIMITS.maxPackageBytes) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: "rule package exceeds byte limit"
});
}
});
const RULE_SYNC_PATH = "/api/rules/sync";
const MAX_RULE_PACKAGES = 256;
const RuleSyncKnownPackageSchema = objectType({
packageId: StableIdSchema,
releaseSequence: numberType().int().nonnegative(),
contentHash: RuleContentHashSchema
}).strict();
const RuleReleaseChannelSchema = enumType(["stable", "candidate"]);
const RuleRolloutPercentSchema = unionType([
literalType(5),
literalType(20),
literalType(50),
literalType(100)
]);
const RuleReleaseContextSchema = objectType({
releaseId: StableIdSchema,
channel: RuleReleaseChannelSchema,
rolloutPercent: RuleRolloutPercentSchema,
cohortBucket: numberType().int().min(0).max(9999)
}).strict();
function isRuleCandidateTestDelivery(value) {
return value.channel === "candidate" && value.cohortBucket >= value.rolloutPercent * 100;
}
const RulePackageSummarySchema = RuleReleaseContextSchema.extend({
packageId: StableIdSchema,
platform: StableIdSchema,
version: VersionSchema,
releaseSequence: numberType().int().nonnegative(),
contentHash: RuleContentHashSchema,
issuedAt: numberType().int().nonnegative(),
rollbackAuthorization: RuleRollbackAuthorizationSchema.optional()
}).strict();
objectType({
engineVersion: VersionSchema,
known: arrayType(RuleSyncKnownPackageSchema).max(MAX_RULE_PACKAGES)
}).strict().superRefine((request, ctx) => {
const packageIds = request.known.map((item) => item.packageId);
if (new Set(packageIds).size !== packageIds.length) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["known"],
message: "duplicate package id"
});
}
});
const sameRollbackAuthorization = (left, right) => (left == null ? void 0 : left.toVersion) === (right == null ? void 0 : right.toVersion) && (left == null ? void 0 : left.authorizationId) === (right == null ? void 0 : right.authorizationId);
const RuleSyncResponseSchema = objectType({
code: nativeEnumType(AiAskCode),
checkedAt: numberType().int().nonnegative(),
latest: arrayType(RulePackageSummarySchema).max(MAX_RULE_PACKAGES),
update: RulePackageSchema.nullable(),
hasMore: booleanType()
}).strict().superRefine((response, ctx) => {
const packageIds = response.latest.map((item) => item.packageId);
if (new Set(packageIds).size !== packageIds.length) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["latest"],
message: "duplicate package id"
});
}
if (response.update == null && response.hasMore) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["hasMore"],
message: "hasMore requires an update package"
});
}
if (response.update) {
const summary = response.latest.find(
(item) => {
var _a2;
return item.packageId === ((_a2 = response.update) == null ? void 0 : _a2.packageId) && item.releaseSequence === response.update.releaseSequence && item.contentHash === response.update.contentHash;
}
);
if (!summary) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["update"],
message: "update package is missing from latest summaries"
});
} else if (!sameRollbackAuthorization(
response.update.rollbackAuthorization,
summary.rollbackAuthorization
)) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["update", "rollbackAuthorization"],
message: "rollback authorization summary mismatch"
});
}
}
});
const REPORT_PATH = "/api/report";
const ReportStageSchema = enumType([
"resolve",
"match",
"lifecycle",
"capture",
"decode",
"query",
"safety",
"fill",
"verify",
"update"
]);
const ReportReasonSchema = enumType([
"no_match",
"invalid_match_result",
"zero_question",
"selector_zero",
"selector_many",
"decode_failed",
"query_failed",
"unsafe_answer",
"missing_binding",
"disconnected",
"stale_dom",
"ambiguous_binding",
"shape_mismatch",
"partial_not_allowed",
"adapter_rejected",
"fill_failed",
"verify_failed",
"timeout",
"budget_exceeded",
"unknown_primitive",
/** 规则在 capture 阶段自己挂了(选择器漂移、原语被拒、结果不合法等)。 */
"rule_failed",
"update_failed",
"unsupported_question"
]);
const ReportRuleSourceSchema = enumType([
/**
* 历史值:客户端「内置基线」档已于 2026-08-20 从 `RuleStore` 删除(2026-08-01
* 下线、2026-08-18 用户裁定固化)。**枚举里留着它只为读旧上报行**——后台的
* 聚合/明细响应也用这个 schema 校验,摘掉它会让库里既有的旧行整条响应报错。
* 现网客户端不会再产生这个值。
*/
"bundled",
"remote-active",
"remote-lkg",
/**
* 包未下发(#86):壳在、本页本应由某个云端包接管,而客户端一档规则都没有。
* 此档的 packageId 为真实期望包,其余规则字段一律 'missing' 占位、release 缺省;
* 它必须能被上报——否则「规则没下发」在后端表现为流量凭空消失,灰度与自动
* 回滚失去输入。部署顺序:后端先接受此值,客户端后发(老后端会拒收报文)。
*/
"missing"
]);
const ReportRuleContextSchema = objectType({
packageId: stringType().min(1).max(128),
variantId: stringType().min(1).max(128),
source: ReportRuleSourceSchema,
version: stringType().min(1).max(64),
releaseSequence: numberType().int().nonnegative(),
contentHash: stringType().min(1).max(64),
release: RuleReleaseContextSchema.optional()
});
const ReportStageResultSchema = discriminatedUnionType("ok", [
objectType({ stage: ReportStageSchema, ok: literalType(true) }).strict(),
objectType({
stage: ReportStageSchema,
ok: literalType(false),
reason: ReportReasonSchema
}).strict()
]);
const HealthReportSchema = objectType({
schemaVersion: literalType(2),
platform: stringType().min(1).max(32),
clientId: stringType().min(8).max(64),
scriptVersion: stringType().max(32),
engineVersion: stringType().min(1).max(32),
rule: ReportRuleContextSchema,
mode: literalType("health"),
stages: arrayType(ReportStageResultSchema).min(1).max(16)
});
const DiagnosticItemSchema = objectType({
type: stringType().min(1).max(32),
decodeFailed: booleanType(),
optionCount: numberType().int().nonnegative().max(1e3),
imageCount: numberType().int().nonnegative().max(1e3),
unsupportedReason: enumType(["empty-content"]).optional()
}).strict();
const DiagnosticPayloadSchema = objectType({
matched: booleanType(),
count: numberType().int().nonnegative().max(1e4),
imageCount: numberType().int().nonnegative().max(1e4),
items: arrayType(DiagnosticItemSchema).max(1e4)
}).strict();
const DiagnosticReportSchema = HealthReportSchema.extend({
mode: literalType("diagnostic"),
diagnostic: DiagnosticPayloadSchema
});
discriminatedUnionType("mode", [
HealthReportSchema,
DiagnosticReportSchema
]);
objectType({ code: nativeEnumType(AiAskCode) });
const BASE64URL = /^[A-Za-z0-9_-]*$/;
function bytesToBase64Url(bytes) {
let binary = "";
const chunkSize = 32768;
for (let offset = 0; offset < bytes.length; offset += chunkSize)
binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
}
function base64UrlToBytes(value) {
if (!BASE64URL.test(value) || value.includes("="))
throw new Error("invalid base64url");
const padding = "=".repeat((4 - value.length % 4) % 4);
let binary;
try {
binary = atob(value.replaceAll("-", "+").replaceAll("_", "/") + padding);
} catch {
throw new Error("invalid base64url");
}
const out = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index++)
out[index] = binary.charCodeAt(index);
if (bytesToBase64Url(out) !== value) throw new Error("invalid base64url");
return out;
}
const utf8Bytes = (value) => new TextEncoder().encode(value);
const utf8Text = (value) => new TextDecoder("utf-8", { fatal: true }).decode(value);
const base64Url = (min, max) => stringType().min(min).max(max).regex(/^[A-Za-z0-9_-]+$/u);
const signature = base64Url(86, 86);
const timestamp = numberType().int().nonnegative();
const kid = stringType().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/u);
const PublicP256JwkSchema = objectType({
kty: literalType("EC"),
crv: literalType("P-256"),
x: base64Url(43, 43),
y: base64Url(43, 43)
}).strict();
const ServerKeyUseSchema = enumType([
"transport-signing",
"ecdh",
"rule-signing"
]);
const ServerKeySchema = objectType({
kid,
use: ServerKeyUseSchema,
publicJwk: PublicP256JwkSchema,
notBefore: timestamp,
expiresAt: timestamp
}).strict();
const UnsignedServerKeysetSchema = objectType({
keysetVersion: numberType().int().positive(),
issuedAt: timestamp,
expiresAt: timestamp,
keys: arrayType(ServerKeySchema).min(1).max(32)
}).strict();
const ServerKeysetSchema = UnsignedServerKeysetSchema.extend({
rootSignature: signature
}).strict();
const UnsignedBootstrapChallengeSchema = objectType({
protocolVersion: literalType(1),
minClientVersion: stringType().min(1).max(32),
serverTime: timestamp,
challenge: base64Url(22, 128),
challengeExpiresAt: timestamp,
keysetVersion: numberType().int().positive(),
keysetHash: base64Url(43, 43),
signingKid: kid
}).strict();
const BootstrapChallengeSchema = UnsignedBootstrapChallengeSchema.extend(
{ signature }
).strict();
const BootstrapDocumentSchema = objectType({
keyset: ServerKeysetSchema,
challenge: BootstrapChallengeSchema
}).strict();
const SecurityScopeSchema = enumType(["report", "user", "admin"]);
const sessionOpenFields = {
protocolVersion: literalType(1),
challenge: base64Url(22, 128),
deviceId: base64Url(43, 43),
devicePublicJwk: PublicP256JwkSchema,
ecdhKid: kid,
clientEcdhPublicJwk: PublicP256JwkSchema,
timestamp,
nonce: base64Url(22, 128),
signature
};
objectType({
...sessionOpenFields,
requestedScope: enumType(["report", "user"])
}).strict();
const AdminSessionOpenRequestSchema = objectType(sessionOpenFields).strict();
objectType({
username: stringType().min(3).max(64),
password: stringType().min(1).max(256)
}).strict();
const SessionOpenResponseSchema = objectType({
protocolVersion: literalType(1),
signingKid: kid,
ecdhKid: kid,
serverEcdhPublicJwk: PublicP256JwkSchema,
serverNonce: base64Url(22, 128),
iv: base64Url(16, 16),
ciphertext: base64Url(1, 16384),
signature
}).strict();
const SessionOpenPlaintextSchema = objectType({
sessionId: base64Url(22, 64),
deviceId: base64Url(43, 43),
grantedScope: SecurityScopeSchema,
issuedAt: timestamp,
expiresAt: timestamp
}).strict();
const secureRequestFields = {
v: literalType(1),
sessionId: base64Url(22, 64),
requestId: stringType().uuid(),
timestamp,
nonce: base64Url(22, 128),
iv: base64Url(16, 16),
ciphertext: base64Url(1, 14e5),
signature
};
objectType(secureRequestFields).strict();
const SecureResponseEnvelopeSchema = objectType({
...secureRequestFields,
kid
}).strict();
function validateServerKeyset(input, now, highestAcceptedVersion = 0) {
const keyset = ServerKeysetSchema.parse(input);
if (keyset.keysetVersion < highestAcceptedVersion)
throw new Error("keyset downgrade");
if (keyset.issuedAt > now || keyset.expiresAt <= now)
throw new Error("keyset expired");
const seen = /* @__PURE__ */ new Set();
let activeEcdh = 0;
for (const key of keyset.keys) {
if (seen.has(key.kid)) throw new Error("duplicate kid");
seen.add(key.kid);
if (key.notBefore >= key.expiresAt || key.expiresAt <= now || key.expiresAt > keyset.expiresAt)
throw new Error("key expired");
if (key.use === "ecdh" && key.notBefore <= now) activeEcdh += 1;
}
if (activeEcdh > 1) throw new Error("ambiguous ECDH key");
}
function validateBootstrapChallenge(input, keyset, now) {
const challenge = BootstrapChallengeSchema.parse(input);
if (challenge.challengeExpiresAt <= now) throw new Error("challenge expired");
if (challenge.keysetVersion !== keyset.keysetVersion)
throw new Error("keyset mismatch");
const signingKey = keyset.keys.find(
(key) => key.kid === challenge.signingKid && key.use === "transport-signing" && key.notBefore <= now && key.expiresAt > now
);
if (!signingKey) throw new Error("invalid signing key");
}
function canonicalize(value) {
if (value === null || typeof value === "boolean") return JSON.stringify(value);
if (typeof value === "string") return JSON.stringify(value);
if (typeof value === "number") {
if (!Number.isFinite(value)) throw new Error("non-finite number");
return JSON.stringify(Object.is(value, -0) ? 0 : value);
}
if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
if (typeof value !== "object") throw new Error("unsupported canonical value");
const record = value;
const entries = Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalize(record[key])}`);
return `{${entries.join(",")}}`;
}
function canonicalPublicJwk(jwk) {
const parsed = PublicP256JwkSchema.parse(jwk);
return canonicalize({
crv: parsed.crv,
kty: parsed.kty,
x: parsed.x,
y: parsed.y
});
}
function requestEnvelopeInput(method, path, envelope) {
return [
"aiask-v1",
method.toUpperCase(),
path,
envelope.sessionId,
envelope.requestId,
String(envelope.timestamp),
envelope.nonce,
envelope.iv,
envelope.ciphertext
].join("\n");
}
function responseEnvelopeInput(method, path, envelope) {
return [
"aiask-v1-response",
method.toUpperCase(),
path,
envelope.kid,
envelope.sessionId,
envelope.requestId,
String(envelope.timestamp),
envelope.nonce,
envelope.iv,
envelope.ciphertext
].join("\n");
}
function requestEnvelopeAad(method, path, envelope) {
return [
"aiask-v1-aad",
method.toUpperCase(),
path,
envelope.sessionId,
envelope.requestId,
String(envelope.timestamp),
envelope.nonce,
envelope.iv
].join("\n");
}
function responseEnvelopeAad(method, path, envelope) {
return [
"aiask-v1-response-aad",
method.toUpperCase(),
path,
envelope.kid,
envelope.sessionId,
envelope.requestId,
String(envelope.timestamp),
envelope.nonce,
envelope.iv
].join("\n");
}
function unsignedServerKeyset(keyset) {
const { rootSignature: _rootSignature, ...unsigned } = keyset;
return unsigned;
}
function unsignedBootstrapChallenge(challenge) {
const { signature: _signature, ...unsigned } = challenge;
return unsigned;
}
const serverKeysetSigningInput = (keyset) => canonicalize(keyset);
const bootstrapChallengeSigningInput = (challenge) => canonicalize(challenge);
function sessionOpenRequestInput(path, request) {
return `aiask-v1-session-open
POST
${path}
${canonicalize(request)}`;
}
function sessionOpenResponseInput(path, response) {
return `aiask-v1-session-open-response
POST
${path}
${canonicalize(response)}`;
}
function sessionOpenResponseAad(path, response) {
return `aiask-v1-session-open-aad
POST
${path}
${canonicalize(response)}`;
}
const handshakeTranscript = (context) => canonicalize(context);
const trafficTranscript = (context) => canonicalize(context);
const subtle = () => globalThis.crypto.subtle;
const arrayBuffer = (value) => {
const copy = new Uint8Array(value.length);
copy.set(value);
return copy.buffer;
};
const publicJwk = (jwk) => {
const parsed = JSON.parse(canonicalPublicJwk(jwk));
return { ...parsed, ext: true };
};
const privateJwk = (jwk) => {
if (jwk.kty !== "EC" || jwk.crv !== "P-256" || typeof jwk.x !== "string" || typeof jwk.y !== "string" || typeof jwk.d !== "string")
throw new Error("invalid private P-256 JWK");
return {
kty: "EC",
crv: "P-256",
x: jwk.x,
y: jwk.y,
d: jwk.d,
ext: true
};
};
const importEcdsaPublicJwk = (jwk) => subtle().importKey(
"jwk",
publicJwk(jwk),
{ name: "ECDSA", namedCurve: "P-256" },
true,
["verify"]
);
const importEcdsaPrivateJwk = (jwk) => subtle().importKey(
"jwk",
privateJwk(jwk),
{ name: "ECDSA", namedCurve: "P-256" },
false,
["sign"]
);
const importEcdhPublicJwk = (jwk) => subtle().importKey(
"jwk",
publicJwk(jwk),
{ name: "ECDH", namedCurve: "P-256" },
true,
[]
);
async function generateEcdsaDeviceKeyPair() {
return await subtle().generateKey(
{ name: "ECDSA", namedCurve: "P-256" },
true,
["sign", "verify"]
);
}
async function generateEcdhKeyPair() {
return await subtle().generateKey(
{ name: "ECDH", namedCurve: "P-256" },
true,
["deriveBits"]
);
}
const exportPublicJwk = async (key) => {
const exported = await subtle().exportKey("jwk", key);
return JSON.parse(
canonicalPublicJwk({
kty: exported.kty,
crv: exported.crv,
x: exported.x,
y: exported.y
})
);
};
const exportPrivateJwk = (key) => subtle().exportKey("jwk", key);
async function signEcdsaP1363(privateKey, data) {
const signature2 = new Uint8Array(
await subtle().sign(
{ name: "ECDSA", hash: "SHA-256" },
privateKey,
arrayBuffer(data)
)
);
if (signature2.length !== 64) throw new Error("invalid ECDSA signature length");
return bytesToBase64Url(signature2);
}
async function verifyEcdsaP1363(publicKey, data, signature2) {
let bytes;
try {
bytes = base64UrlToBytes(signature2);
} catch {
return false;
}
if (bytes.length !== 64) return false;
return subtle().verify(
{ name: "ECDSA", hash: "SHA-256" },
publicKey,
arrayBuffer(bytes),
arrayBuffer(data)
);
}
async function deriveEcdhSecret(privateKey, publicKey) {
return new Uint8Array(
await subtle().deriveBits(
{ name: "ECDH", public: publicKey },
privateKey,
256
)
);
}
async function sha256(data) {
return new Uint8Array(await subtle().digest("SHA-256", arrayBuffer(data)));
}
const sha256Base64Url = async (data) => bytesToBase64Url(await sha256(data));
const fingerprintPublicJwk = (jwk) => sha256Base64Url(utf8Bytes(canonicalPublicJwk(jwk)));
async function hkdf(sharedSecret, saltLabel, infoLabel, transcript) {
const material = await subtle().importKey(
"raw",
arrayBuffer(sharedSecret),
"HKDF",
false,
["deriveBits"]
);
const salt = await sha256(utf8Bytes(`${saltLabel}
${transcript}`));
return new Uint8Array(
await subtle().deriveBits(
{
name: "HKDF",
hash: "SHA-256",
salt: arrayBuffer(salt),
info: arrayBuffer(utf8Bytes(`${infoLabel}
${transcript}`))
},
material,
256
)
);
}
const deriveHandshakeKey = (sharedSecret, context) => {
const transcript = handshakeTranscript(context);
return hkdf(
sharedSecret,
"aiask-v1-handshake-salt",
"aiask-v1-handshake-key",
transcript
);
};
async function deriveTrafficKeys(sharedSecret, context) {
const transcript = trafficTranscript(context);
const [c2sKey, s2cKey] = await Promise.all([
hkdf(sharedSecret, "aiask-v1-traffic-salt", "aiask-v1-c2s-key", transcript),
hkdf(sharedSecret, "aiask-v1-traffic-salt", "aiask-v1-s2c-key", transcript)
]);
return { c2sKey, s2cKey };
}
const importAesKey = (raw, usage) => subtle().importKey(
"raw",
arrayBuffer(raw),
{ name: "AES-GCM", length: 256 },
false,
usage
);
async function aesGcmEncrypt(rawKey, iv, plaintext, aad) {
if (rawKey.length !== 32 || iv.length !== 12)
throw new Error("invalid AES-GCM key or IV");
const key = await importAesKey(rawKey, ["encrypt"]);
return new Uint8Array(
await subtle().encrypt(
{
name: "AES-GCM",
iv: arrayBuffer(iv),
additionalData: arrayBuffer(aad),
tagLength: 128
},
key,
arrayBuffer(plaintext)
)
);
}
async function aesGcmDecrypt(rawKey, iv, ciphertext, aad) {
if (rawKey.length !== 32 || iv.length !== 12)
throw new Error("invalid AES-GCM key or IV");
const key = await importAesKey(rawKey, ["decrypt"]);
return new Uint8Array(
await subtle().decrypt(
{
name: "AES-GCM",
iv: arrayBuffer(iv),
additionalData: arrayBuffer(aad),
tagLength: 128
},
key,
arrayBuffer(ciphertext)
)
);
}
async function verifyServerKeysetSignature(rootPublicKey, keyset) {
return verifyEcdsaP1363(
rootPublicKey,
utf8Bytes(serverKeysetSigningInput(unsignedServerKeyset(keyset))),
keyset.rootSignature
);
}
const serverKeysetHash = (keyset) => sha256Base64Url(
utf8Bytes(serverKeysetSigningInput(unsignedServerKeyset(keyset)))
);
async function verifyBootstrapChallengeSignature(keyset, challenge) {
const signingKey = keyset.keys.find(
(key) => key.kid === challenge.signingKid && key.use === "transport-signing"
);
if (!signingKey) return false;
const publicKey = await importEcdsaPublicJwk(signingKey.publicJwk);
return verifyEcdsaP1363(
publicKey,
utf8Bytes(
bootstrapChallengeSigningInput(unsignedBootstrapChallenge(challenge))
),
challenge.signature
);
}
const withoutSignature = (pkg) => {
const { signature: _signature, ...unsigned } = pkg;
return unsigned;
};
const withoutContentHashAndSignature = (pkg) => {
const { contentHash: _contentHash, signature: _signature, ...hashable } = pkg;
return hashable;
};
function rulePackageContentHashInput(input) {
const pkg = RulePackageSchema.parse(input);
return canonicalize(withoutContentHashAndSignature(pkg));
}
function rulePackageSignatureInput(input) {
const pkg = RulePackageSchema.parse(input);
return canonicalize(withoutSignature(pkg));
}
const computeRulePackageContentHash = (input) => sha256Base64Url(utf8Bytes(rulePackageContentHashInput(input)));
function parseVersion$1(value) {
const withoutBuild = value.split("+", 1)[0] ?? value;
const prereleaseIndex = withoutBuild.indexOf("-");
const coreText = prereleaseIndex === -1 ? withoutBuild : withoutBuild.slice(0, prereleaseIndex);
const prereleaseText = prereleaseIndex === -1 ? void 0 : withoutBuild.slice(prereleaseIndex + 1);
const parts = coreText.split(".").map(Number);
if (parts.length !== 3 || parts.some((part) => !Number.isSafeInteger(part) || part < 0))
throw new Error(`invalid version: ${value}`);
return {
core: parts,
prerelease: prereleaseText ? prereleaseText.split(".").map((part) => /^\d+$/u.test(part) ? Number(part) : part) : []
};
}
function compareRuleVersions$1(leftValue, rightValue) {
const left = parseVersion$1(leftValue);
const right = parseVersion$1(rightValue);
for (let index = 0; index < 3; index += 1) {
const difference = left.core[index] - right.core[index];
if (difference !== 0) return Math.sign(difference);
}
if (left.prerelease.length === 0 && right.prerelease.length === 0) return 0;
if (left.prerelease.length === 0) return 1;
if (right.prerelease.length === 0) return -1;
const length = Math.max(left.prerelease.length, right.prerelease.length);
for (let index = 0; index < length; index += 1) {
const leftPart = left.prerelease[index];
const rightPart = right.prerelease[index];
if (leftPart == null) return -1;
if (rightPart == null) return 1;
if (leftPart === rightPart) continue;
if (typeof leftPart === "number" && typeof rightPart === "string") return -1;
if (typeof leftPart === "string" && typeof rightPart === "number") return 1;
return leftPart < rightPart ? -1 : 1;
}
return 0;
}
const IMPORT_BRIDGE_ORIGIN = "https://www.aiask.site";
const IMPORT_BRIDGE_VERSION = 1;
const IMPORT_BRIDGE_CHANNEL = "aiask-import";
const IMPORT_BRIDGE_REPLY_CHANNEL = "aiask-import-reply";
const IMPORT_BRIDGE_ERROR_REASONS = [
/** 快照解析/校验没过。页面自己建的快照走到这里说明是 bug,不是用户输入问题。 */
"invalid-snapshot",
/** 快照没问题,但写入过程失败。 */
"import-failed"
];
const ImportBridgeErrorReasonSchema = enumType(IMPORT_BRIDGE_ERROR_REASONS);
const requestId = stringType().uuid();
const snapshot = stringType().min(1);
const count = numberType().int().min(0);
const requestBase = {
channel: literalType(IMPORT_BRIDGE_CHANNEL),
v: literalType(IMPORT_BRIDGE_VERSION),
requestId
};
const ImportBridgeRequestSchema = discriminatedUnionType("kind", [
objectType({ ...requestBase, kind: literalType("ping") }).strict(),
objectType({ ...requestBase, kind: literalType("preview"), snapshot }).strict(),
objectType({ ...requestBase, kind: literalType("commit"), snapshot }).strict()
]);
const replyBase = {
channel: literalType(IMPORT_BRIDGE_REPLY_CHANNEL),
v: literalType(IMPORT_BRIDGE_VERSION),
requestId
};
discriminatedUnionType("kind", [
objectType({
...replyBase,
kind: literalType("pong"),
scriptVersion: stringType().min(1)
}).strict(),
objectType({
...replyBase,
kind: literalType("preview"),
fileCount: count,
added: count,
replaced: count,
skipped: count
}).strict(),
objectType({
...replyBase,
kind: literalType("commit"),
added: count,
replaced: count,
skipped: count
}).strict(),
objectType({
...replyBase,
kind: literalType("error"),
reason: ImportBridgeErrorReasonSchema
}).strict()
]);
function parseImportBridgeRequest(data) {
const parsed = ImportBridgeRequestSchema.safeParse(data);
return parsed.success ? parsed.data : null;
}
function importBridgePreviewReply(requestId2, counts) {
return {
channel: IMPORT_BRIDGE_REPLY_CHANNEL,
v: IMPORT_BRIDGE_VERSION,
requestId: requestId2,
kind: "preview",
fileCount: counts.fileCount,
added: counts.added,
replaced: counts.replaced,
skipped: counts.skipped
};
}
function importBridgeCommitReply(requestId2, counts) {
return {
channel: IMPORT_BRIDGE_REPLY_CHANNEL,
v: IMPORT_BRIDGE_VERSION,
requestId: requestId2,
kind: "commit",
added: counts.added,
replaced: counts.replaced,
skipped: counts.skipped
};
}
const BUSY = Object.freeze({
code: AiAskCode.Busy,
found: false,
result: null
});
const DEFAULT_TIMEOUT_MS$3 = 8e3;
class RelayClient {
constructor(transport, baseUrl, timeoutMs = DEFAULT_TIMEOUT_MS$3) {
this.transport = transport;
this.baseUrl = baseUrl;
this.timeoutMs = timeoutMs;
}
async search(req, idempotencyKey) {
let timer;
try {
const deadline = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error("deadline")), this.timeoutMs);
});
const res = await Promise.race([
this.transport.send({
url: this.baseUrl + SEARCH_PATH,
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey
},
body: JSON.stringify(req),
timeoutMs: this.timeoutMs
}),
deadline
]);
if (res.status < 200 || res.status >= 300) return BUSY;
const parsed = SearchUnitResponseSchema.safeParse(JSON.parse(res.body));
return parsed.success ? parsed.data : BUSY;
} catch {
return BUSY;
} finally {
clearTimeout(timer);
}
}
}
class RuleRuntime {
constructor(adapters) {
this.adapters = adapters;
}
resolve(ctx) {
return this.adapters.find((a) => a.match(ctx)) ?? null;
}
}
function collapseWs(s) {
return s.replace(/\s+/g, " ").trim();
}
function stripOptionPrefix(text) {
return text.replace(
/^\s*(?:[((][A-Za-z0-9]{1,2}[))]|[A-Za-z0-9]{1,2}\s*[.、.,::])\s*/,
""
).trim();
}
function normalizeForMatch(text) {
return normalizeQuestionContentForMatch(text).replace(/\s+/g, "").replace(/^[A-Za-z][.、.,]/, "");
}
const TRUE_TOK = /* @__PURE__ */ new Set([
"对",
"正确",
"√",
"✓",
"✔",
"☑",
"是",
"t",
"true",
"y",
"yes",
"1"
]);
const FALSE_TOK = /* @__PURE__ */ new Set([
"错",
"错误",
"×",
"✗",
"✘",
"☒",
"x",
"否",
"f",
"false",
"n",
"no",
"0"
]);
function normalizeTruth(s) {
const t = s.replace(/\s/g, "").toLowerCase();
if (!t) return null;
if (TRUE_TOK.has(t)) return "对";
if (FALSE_TOK.has(t)) return "错";
return null;
}
const LEAF_TYPE_ALIASES = {
single: "single",
single_choice: "single",
single_selection: "single",
单选: "single",
单选题: "single",
multiple: "multiple",
multiple_choice: "multiple",
multiple_selection: "multiple",
多选: "multiple",
多选题: "multiple",
judge: "judge",
judgement: "judge",
judgment: "judge",
true_false: "judge",
true_or_false: "judge",
判断: "judge",
判断题: "judge",
fill: "fill",
completion: "fill",
blank: "fill",
fill_in_blank: "fill",
填空: "fill",
填空题: "fill",
short_answer: "short_answer",
subjective: "short_answer",
essay: "short_answer",
简答: "short_answer",
简答题: "short_answer"
};
function normalizeLeafQuestionType(value) {
if (!(value == null ? void 0 : value.trim())) return null;
return LEAF_TYPE_ALIASES[value.trim().toLocaleLowerCase().replace(/[\s-]+/gu, "_")] ?? null;
}
class DomContentError extends Error {
constructor(code, message) {
super(message);
this.code = code;
this.name = "DomContentError";
}
}
function serializeDomQuestionContent(element, options = {}) {
const stripSelectors = options.stripSelectors ?? [];
const maxNodes = options.maxNodes ?? 5e3;
if (!Number.isInteger(maxNodes) || maxNodes <= 0 || maxNodes > 5e4)
throw new DomContentError(
"budget_exceeded",
"invalid DOM content node limit"
);
for (const selector of stripSelectors) {
try {
element.matches(selector);
} catch (error) {
throw new DomContentError(
"invalid_selector",
error instanceof Error ? error.message : "invalid strip selector"
);
}
}
const out = [];
let visited = 0;
const walk = (node) => {
var _a2, _b;
if ((_a2 = options.signal) == null ? void 0 : _a2.aborted)
throw new DomContentError("cancelled", "DOM content capture cancelled");
visited += 1;
if (visited > maxNodes)
throw new DomContentError(
"budget_exceeded",
"DOM content node budget exceeded"
);
if (node.nodeType === node.TEXT_NODE) {
out.push(serializeQuestionText(node.textContent ?? ""));
return;
}
const document2 = node.ownerDocument;
const view = document2 == null ? void 0 : document2.defaultView;
if (!view || !(node instanceof view.Element)) return;
if (node !== element && stripSelectors.some((selector) => node.matches(selector)))
return;
const tag = node.tagName.toLowerCase();
if (tag === "script" || tag === "style" || tag === "noscript") return;
if (tag === "img") {
const raw = (_b = node.getAttribute("src")) == null ? void 0 : _b.trim();
if (!raw) return;
try {
const url = new URL(raw, document2.baseURI);
if (url.protocol === "http:" || url.protocol === "https:")
out.push(serializeImageToken(url.href));
} catch {
return;
}
return;
}
for (const child of node.childNodes) walk(child);
};
walk(element);
return collapseWs(out.join(""));
}
function waitUntil(cond, opts) {
const interval = opts.interval ?? 100;
return new Promise((resolve) => {
var _a2, _b;
if (cond()) return resolve(true);
if ((_a2 = opts.signal) == null ? void 0 : _a2.aborted) return resolve(false);
let waited = 0;
const onAbort = () => {
cleanup();
resolve(false);
};
const timer = setInterval(() => {
var _a3;
if (cond()) {
cleanup();
resolve(true);
return;
}
waited += interval;
if (((_a3 = opts.signal) == null ? void 0 : _a3.aborted) || waited >= opts.timeout) {
cleanup();
resolve(false);
}
}, interval);
(_b = opts.signal) == null ? void 0 : _b.addEventListener("abort", onAbort, { once: true });
function cleanup() {
var _a3;
clearInterval(timer);
(_a3 = opts.signal) == null ? void 0 : _a3.removeEventListener("abort", onAbort);
}
});
}
const issuedCapabilities = /* @__PURE__ */ new WeakMap();
function createSafetyCapability() {
const capability = Object.freeze({
toJSON() {
throw new Error("safety capability cannot be serialized");
}
});
return capability;
}
function operationMatches$1(left, right) {
if (left.kind !== right.kind) return false;
if (left.kind === "choose" && right.kind === "choose")
return left.optionId === right.optionId;
if (left.kind === "write" && right.kind === "write")
return left.slotId === right.slotId && left.value === right.value;
return left.kind === "pair" && right.kind === "pair" && left.leftId === right.leftId && left.rightId === right.rightId;
}
function assertSafetyCapability(capability) {
if (typeof capability !== "object" && typeof capability !== "function" || capability === null || !issuedCapabilities.has(capability)) {
throw new Error("invalid safety capability");
}
}
function safetyPlanForCapability(capability) {
assertSafetyCapability(capability);
return issuedCapabilities.get(capability);
}
function assertSafetyOperation(capability, operation) {
const plan = safetyPlanForCapability(capability);
if (!plan.operations.some((candidate) => operationMatches$1(candidate, operation)))
throw new Error("operation is not allowed by safety capability");
}
function hasDuplicates(values) {
return new Set(values).size !== values.length;
}
function validateBinding(unit, binding) {
if (binding.path !== unit.unitPath) {
return { kind: "unsafe", reason: "missing-binding" };
}
if (!binding.connected) return { kind: "unsafe", reason: "disconnected" };
if (binding.capturedFingerprint !== unit.sourceNodeHash || binding.currentFingerprint !== binding.capturedFingerprint) {
return { kind: "unsafe", reason: "stale" };
}
if (hasDuplicates(binding.optionIds) || hasDuplicates(binding.slotIds) || hasDuplicates(binding.leftIds) || hasDuplicates(binding.rightIds)) {
return { kind: "unsafe", reason: "ambiguous-binding" };
}
return null;
}
function allPresent(expected, actual) {
const available = new Set(actual);
return expected.every((id) => available.has(id));
}
function buildFillPlan(node, unit, answer, binding) {
const invalidBinding = validateBinding(unit, binding);
if (invalidBinding) return invalidBinding;
let operations;
let atomic = true;
if (answer.kind === "choice") {
if (node.kind !== "leaf" || unit.answerShape.kind !== "choice" || answer.optionIds.length === 0 || hasDuplicates(answer.optionIds) || !allPresent(answer.optionIds, binding.optionIds)) {
return { kind: "unsafe", reason: "missing-binding" };
}
if (answer.optionIds.length < unit.answerShape.min || answer.optionIds.length > unit.answerShape.max) {
return { kind: "unsafe", reason: "shape-mismatch" };
}
operations = answer.optionIds.map((optionId) => ({
kind: "choose",
optionId
}));
} else if (answer.kind === "slots") {
if (node.kind !== "leaf" || unit.answerShape.kind !== "slots" || answer.slots.length === 0 || hasDuplicates(answer.slots.map((slot) => slot.slotId)) || !allPresent(
answer.slots.map((slot) => slot.slotId),
binding.slotIds
)) {
return { kind: "unsafe", reason: "missing-binding" };
}
if (!allPresent(
unit.answerShape.slotIds,
answer.slots.map((slot) => slot.slotId)
) && node.fillPolicy === "atomic") {
return { kind: "unsafe", reason: "shape-mismatch" };
}
if (!allPresent(
answer.slots.map((slot) => slot.slotId),
unit.answerShape.slotIds
)) {
return { kind: "unsafe", reason: "shape-mismatch" };
}
const writes = answer.slots.map((slot) => ({
kind: "write",
slotId: slot.slotId,
value: slot.values.find((value) => value.trim()) ?? ""
}));
if (writes.some((write) => !write.value)) {
return { kind: "unsafe", reason: "shape-mismatch" };
}
operations = writes;
atomic = node.fillPolicy === "atomic";
} else {
if (node.kind !== "matching" || unit.answerShape.kind !== "matching-pair" || unit.answerShape.leftId !== answer.leftId || !binding.leftIds.includes(answer.leftId) || !binding.rightIds.includes(answer.rightId) || !unit.answerShape.rightIds.includes(answer.rightId)) {
return { kind: "unsafe", reason: "missing-binding" };
}
operations = [
{ kind: "pair", leftId: answer.leftId, rightId: answer.rightId }
];
atomic = node.fillPolicy === "atomic";
}
const safetyCapability = createSafetyCapability();
const plan = Object.freeze({
path: unit.unitPath,
atomic,
operations: Object.freeze(
operations.map((operation) => Object.freeze({ ...operation }))
),
fingerprint: binding.currentFingerprint,
safetyCapability
});
issuedCapabilities.set(safetyCapability, plan);
return {
kind: "safe",
plan
};
}
const failed = (path) => ({
complete: false,
plans: [],
unsafePaths: [path]
});
function buildTreeFillPlans(root, answerTree, bindings) {
const units = flattenQuestionTree(root);
const unitByPath = new Map(units.map((unit) => [unit.unitPath, unit]));
const visitLeaf = (node, answer) => {
if (answer.kind !== "leaf" || answer.path !== node.path || answer.status !== "hit" || !answer.answer) {
return failed(node.path);
}
const unit = unitByPath.get(node.path);
const binding = bindings.get(node.path);
if (!unit || !binding) return failed(node.path);
const result2 = buildFillPlan(node, unit, answer.answer, binding);
return result2.kind === "safe" ? { complete: true, plans: [result2.plan], unsafePaths: [] } : failed(node.path);
};
const visitMatching = (node, answer) => {
if (answer.kind !== "matching" || answer.path !== node.path) {
return failed(node.path);
}
const rightIds = [];
const plans = [];
const unsafePaths = [];
for (const left of node.left) {
const pair = answer.pairs.find(
(candidate) => candidate.leftId === left.id
);
const unit = units.find(
(candidate) => candidate.answerShape.kind === "matching-pair" && candidate.answerShape.leftId === left.id
);
const pairPath = (unit == null ? void 0 : unit.unitPath) ?? `${node.path}/pairs/${left.id}`;
const binding = unit ? bindings.get(unit.unitPath) : void 0;
if (!pair || pair.status !== "hit" || !pair.rightId || !pair.displayValue || !unit || !binding) {
unsafePaths.push(pairPath);
continue;
}
rightIds.push(pair.rightId);
const payload = {
kind: "matching-pair",
leftId: left.id,
rightId: pair.rightId,
displayValue: pair.displayValue
};
const result2 = buildFillPlan(node, unit, payload, binding);
if (result2.kind === "safe") plans.push(result2.plan);
else unsafePaths.push(pairPath);
}
if (node.cardinality === "one-to-one" && new Set(rightIds).size !== rightIds.length) {
return failed(node.path);
}
const complete = unsafePaths.length === 0 && plans.length === node.left.length;
if (!complete && node.fillPolicy === "atomic") return failed(node.path);
return { complete, plans, unsafePaths };
};
const visit = (node, answer) => {
if (node.kind === "leaf") return visitLeaf(node, answer);
if (node.kind === "matching") return visitMatching(node, answer);
if (answer.kind !== "composite" || answer.path !== node.path) {
return failed(node.path);
}
const childAnswers = new Map(
answer.children.map((child) => [child.path, child])
);
const childResults = node.children.map((child) => {
const childAnswer = childAnswers.get(child.path);
return childAnswer ? visit(child, childAnswer) : failed(child.path);
});
const complete = childResults.every((result2) => result2.complete);
if (!complete && node.fillPolicy === "atomic") return failed(node.path);
return {
complete,
plans: childResults.flatMap((result2) => result2.plans),
unsafePaths: childResults.flatMap((result2) => result2.unsafePaths)
};
};
const result = visit(root, answerTree);
return {
blocked: result.plans.length === 0 && result.unsafePaths.length > 0 && (root.kind === "composite" ? root.fillPolicy === "atomic" : root.kind === "matching" ? root.fillPolicy === "atomic" : false),
plans: result.plans,
unsafePaths: result.unsafePaths
};
}
const MAX_HARD_BINDINGS$1 = 5e3;
const FINGERPRINT_PATTERN = /^[a-f0-9]{64}$/u;
class RuleBindingRegistryError extends Error {
constructor(code, message) {
super(message);
this.code = code;
this.name = "RuleBindingRegistryError";
}
}
function validId$1(value) {
return value.length > 0 && value.length <= 256;
}
function targetKey(target) {
if (target.kind === "choose") return `choose:${target.optionId}`;
if (target.kind === "write") return `write:${target.slotId}`;
return `pair:${target.leftId}:${target.rightId}`;
}
function operationMatches(target, operation) {
if (target.kind !== operation.kind) return false;
if (target.kind === "choose" && operation.kind === "choose")
return target.optionId === operation.optionId;
if (target.kind === "write" && operation.kind === "write")
return target.slotId === operation.slotId;
return target.kind === "pair" && operation.kind === "pair" && target.leftId === operation.leftId && target.rightId === operation.rightId;
}
function unique(values) {
return [...new Set(values)];
}
class RuleBindingRegistry {
constructor(options) {
__publicField(this, _a, "RuleBindingRegistry");
__publicField(this, "entriesByPath", /* @__PURE__ */ new Map());
__publicField(this, "maxBindings");
__publicField(this, "sealed", false);
__publicField(this, "disposed", false);
if (!Number.isInteger(options.maxBindings) || options.maxBindings <= 0 || options.maxBindings > MAX_HARD_BINDINGS$1)
throw new RuleBindingRegistryError(
"invalid_options",
"invalid binding registry limit"
);
this.maxBindings = options.maxBindings;
}
get size() {
return this.disposed ? 0 : this.entriesByPath.size;
}
register(registration) {
if (this.sealed || this.disposed)
throw new RuleBindingRegistryError("sealed", "binding registry is sealed");
if (this.entriesByPath.has(registration.path))
throw new RuleBindingRegistryError(
"duplicate_path",
`duplicate binding path: ${registration.path}`
);
if (this.entriesByPath.size >= this.maxBindings)
throw new RuleBindingRegistryError(
"binding_limit",
"binding registry limit exceeded"
);
if (!registration.path.startsWith("/") || registration.path.length > 1024 || !FINGERPRINT_PATTERN.test(registration.capturedFingerprint) || typeof registration.readCurrentFingerprint !== "function" || !Array.isArray(registration.targets) || registration.targets.length === 0)
throw new RuleBindingRegistryError(
"invalid_registration",
"invalid binding registration"
);
const targetKeys = /* @__PURE__ */ new Set();
for (const target of registration.targets) {
const ids = target.kind === "choose" ? [target.optionId] : target.kind === "write" ? [target.slotId] : [target.leftId, target.rightId];
if (ids.some((id) => !validId$1(id)) || typeof target.isConnected !== "function" || typeof target.apply !== "function" || typeof target.verify !== "function")
throw new RuleBindingRegistryError(
"invalid_registration",
"invalid binding target"
);
const key = targetKey(target);
if (targetKeys.has(key))
throw new RuleBindingRegistryError(
"duplicate_target",
`duplicate binding target: ${key}`
);
targetKeys.add(key);
}
this.entriesByPath.set(registration.path, {
path: registration.path,
capturedFingerprint: registration.capturedFingerprint,
readCurrentFingerprint: registration.readCurrentFingerprint,
targets: Object.freeze([...registration.targets])
});
}
seal() {
this.sealed = true;
}
dispose() {
this.disposed = true;
this.sealed = true;
this.entriesByPath.clear();
}
get(path) {
if (this.disposed) return void 0;
const entry = this.entriesByPath.get(path);
if (!entry) return void 0;
let currentFingerprint = "";
let connected = true;
try {
currentFingerprint = entry.readCurrentFingerprint();
if (!FINGERPRINT_PATTERN.test(currentFingerprint)) {
currentFingerprint = "";
connected = false;
}
} catch {
currentFingerprint = "";
connected = false;
}
if (connected) {
try {
connected = entry.targets.every((target) => target.isConnected());
} catch {
connected = false;
}
}
return {
path: entry.path,
capturedFingerprint: entry.capturedFingerprint,
currentFingerprint,
connected,
optionIds: entry.targets.filter(
(target) => target.kind === "choose"
).map((target) => target.optionId),
slotIds: entry.targets.filter(
(target) => target.kind === "write"
).map((target) => target.slotId),
leftIds: unique(
entry.targets.filter(
(target) => target.kind === "pair"
).map((target) => target.leftId)
),
rightIds: unique(
entry.targets.filter(
(target) => target.kind === "pair"
).map((target) => target.rightId)
)
};
}
has(path) {
return !this.disposed && this.entriesByPath.has(path);
}
targetForOperation(path, operation) {
var _a2;
if (this.disposed) return null;
return ((_a2 = this.entriesByPath.get(path)) == null ? void 0 : _a2.targets.find((target) => operationMatches(target, operation))) ?? null;
}
entries() {
return new Map(
[...this.entriesByPath.keys()].flatMap((path) => {
const binding = this.get(path);
return binding ? [[path, binding]] : [];
})
).entries();
}
keys() {
return new Map(this.entries()).keys();
}
values() {
return new Map(this.entries()).values();
}
forEach(callbackfn, thisArg) {
for (const [key, value] of this.entries())
callbackfn.call(thisArg, value, key, this);
}
[(_a = Symbol.toStringTag, Symbol.iterator)]() {
return this.entries();
}
}
const CJK_START = 19968;
const CJK_END = 40870;
const CJK_COUNT = CJK_END - CJK_START;
const cxFontMd5 = (value) => md5Exports.md5(value);
const asRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
function cjkCodePointsFromCmap(font) {
var _a2;
const cmap = asRecord((_a2 = asRecord(font)) == null ? void 0 : _a2.cmap);
if (!cmap || !Array.isArray(cmap.tables)) return null;
const tableIndex = [cmap.p0e4, cmap.p3e1, cmap.p1e0].find(
(value) => typeof value === "number" && Number.isInteger(value) && value >= 0
);
if (typeof tableIndex !== "number") return null;
const table = asRecord(cmap.tables[tableIndex]);
if (!table || typeof table.format !== "number") return null;
const codes = /* @__PURE__ */ new Set();
let inspected = 0;
if (table.format === 0) {
if (!Array.isArray(table.map)) return null;
const upper = Math.min(CJK_END, table.map.length);
for (let code = CJK_START; code < upper; code++)
if (table.map[code]) codes.add(code);
} else if (table.format === 4) {
if (!Array.isArray(table.startCount) || !Array.isArray(table.endCount) || !Array.isArray(table.idDelta) || !Array.isArray(table.idRangeOffset) || !Array.isArray(table.glyphIdArray))
return null;
const rangeCount = Math.min(table.startCount.length, table.endCount.length);
for (let index = 0; index < rangeCount; index++) {
const start = table.startCount[index];
const end = table.endCount[index];
const delta = table.idDelta[index];
const rangeOffset = table.idRangeOffset[index];
if (!Number.isInteger(start) || !Number.isInteger(end) || !Number.isInteger(delta) || !Number.isInteger(rangeOffset))
return null;
const lower = Math.max(CJK_START, start);
const upper = Math.min(CJK_END - 1, end);
for (let code = lower; code <= upper; code++) {
if (++inspected > CJK_COUNT) return [];
const glyph = rangeOffset === 0 ? code + delta & 65535 : table.glyphIdArray[code - start + (rangeOffset >> 1) - (table.idRangeOffset.length - index)];
if (glyph) codes.add(code);
}
}
} else if (table.format === 6) {
if (typeof table.firstCode !== "number" || !Number.isInteger(table.firstCode) || !Array.isArray(table.glyphIdArray))
return null;
const firstIndex = Math.max(0, CJK_START - table.firstCode);
const endIndex = Math.min(
table.glyphIdArray.length,
CJK_END - table.firstCode
);
for (let index = firstIndex; index < endIndex; index++) {
const code = table.firstCode + index;
if (table.glyphIdArray[index]) codes.add(code);
}
} else if (table.format === 12) {
if (!Array.isArray(table.groups)) return null;
for (const group of table.groups) {
if (!Array.isArray(group) || !Number.isInteger(group[0]) || !Number.isInteger(group[1]) || !Number.isInteger(group[2]))
return null;
const start = group[0];
const lower = Math.max(CJK_START, start);
const upper = Math.min(CJK_END - 1, group[1]);
for (let code = lower; code <= upper; code++) {
if (++inspected > CJK_COUNT) return [];
if (group[2] + code - start !== 0) codes.add(code);
}
}
} else {
return null;
}
return [...codes].sort((left, right) => left - right);
}
function base64ToUint8Array(base64) {
const bin = atob(base64);
const arr = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
return arr;
}
function extractCxFontBase64(styleText) {
const m = styleText.match(/base64,([\w\W]+?)'/);
return m ? m[1] : null;
}
function buildCharMap(fontData, table, typr) {
const font = typr.parse(fontData);
const map = {};
const subsetCodes = cjkCodePointsFromCmap(font);
const codes = subsetCodes ?? Array.from({ length: CJK_COUNT }, (_, index) => CJK_START + index);
for (const i of codes) {
const g = typr.U.codeToGlyph(font, i);
if (!g) continue;
const path = typr.U.glyphToPath(font, g);
const real = table[cxFontMd5(JSON.stringify(path)).slice(24)];
if (typeof real === "number") map[i] = real;
}
return map;
}
function applyCharMap(text, map) {
return [...text].map((ch) => {
const real = map[ch.charCodeAt(0)];
return real == null ? ch : String.fromCharCode(real);
}).join("");
}
const LABEL_PATTERN = /^\s*(正确答案|我的答案|答案)\s*[::]?\s*/;
const LETTER_ANSWER_PATTERN = /^[A-Z]([\s,,、;;]*[A-Z])*$/;
const MAX_LETTERS = 26;
const SLOT_INDEX_PATTERN = /^\s*[((]\s*\d+\s*[))]\s*/;
function mapChaoxingHarvestedAnswer(text, options, slotValues = []) {
if (slotValues.length > 0) {
const slots = slotValues.map(
(value) => value.replace(SLOT_INDEX_PATTERN, "").replace(/\s+/g, " ").trim()
);
return slots.every(Boolean) ? slots : [];
}
const stripped = text.replace(LABEL_PATTERN, "").replace(/\s+/g, " ").trim();
if (!stripped) return [];
if (options.length === 0) return [stripped];
const compact = stripped.replace(/[\s,,、;;]/g, "");
if (!LETTER_ANSWER_PATTERN.test(stripped) || compact.length > MAX_LETTERS)
return [stripped];
const values = [];
for (const letter2 of compact) {
const index = letter2.charCodeAt(0) - 65;
const option = options[index];
if (option === void 0) return [];
values.push(option);
}
return values;
}
function stripTitle(title) {
return collapseWs(
title.replace(/^\s*\d+\s*[.、.,]?\s*/, "").replace(/^[((][^()()]*?题[^()()]*?[))]\s*/, "").replace(/【.+?】/, "")
// 去 【类型】
);
}
function compatibleType(unit, itemType) {
if (!(itemType == null ? void 0 : itemType.trim())) return true;
const normalized = normalizeLeafQuestionType(itemType);
if (!normalized) return false;
if (normalized === unit.queryType) return true;
return unit.answerShape.kind === "slots" && (normalized === "fill" || normalized === "short_answer");
}
function matchUniqueOption(value, options, queryType) {
var _a2, _b;
if (queryType === "judge") {
const wantedTruth = normalizeTruth(value);
if (wantedTruth) {
const truthMatches = options.filter(
(option) => normalizeTruth(option.content) === wantedTruth
);
if (truthMatches.length === 1)
return { kind: "matched", option: truthMatches[0] };
if (truthMatches.length > 1) return { kind: "ambiguous" };
}
}
const wanted = normalizeForMatch(value);
if (!wanted) return { kind: "missing" };
const normalized = options.map((option) => ({
option,
value: normalizeForMatch(option.content)
}));
const exact = normalized.filter((option) => option.value === wanted);
if (exact.length === 1)
return { kind: "matched", option: (_a2 = exact[0]) == null ? void 0 : _a2.option };
if (exact.length > 1) return { kind: "ambiguous" };
const contains = normalized.filter((option) => option.value.includes(wanted));
if (contains.length === 1)
return { kind: "matched", option: (_b = contains[0]) == null ? void 0 : _b.option };
return contains.length > 1 ? { kind: "ambiguous" } : { kind: "missing" };
}
function mapOptions(values, unit) {
const matches = [];
for (const value of values) {
const match = matchUniqueOption(value, unit.options, unit.queryType);
if (match.kind !== "matched") return match;
matches.push(match.option);
}
return { kind: "matched", options: matches };
}
function answeredOptionIndexes(unit, answer) {
if (!answer) return [];
if (answer.kind === "choice") {
const wanted = new Set(answer.optionIds);
return unit.options.flatMap(
(option, index) => wanted.has(option.id) ? [index] : []
);
}
if (answer.kind === "matching-pair") {
return unit.options.flatMap(
(option, index) => option.id === answer.rightId ? [index] : []
);
}
if (unit.answerShape.kind !== "slots" || unit.options.length === 0) return [];
const values = new Set(answer.slots.flatMap((slot) => slot.values));
return unit.options.flatMap(
(option, index) => values.has(option.content) ? [index] : []
);
}
function buildAnswerPlan(unit, hit) {
const values = hit.values.map((value) => value.trim());
if (values.length === 0 || values.some((value) => !normalizeForMatch(value))) {
return { kind: "unusable", reason: "empty" };
}
if (!compatibleType(unit, hit.itemType)) {
return { kind: "unusable", reason: "type-mismatch" };
}
if (unit.answerShape.kind === "choice") {
if (values.length < unit.answerShape.min || values.length > unit.answerShape.max || unit.queryType !== "multiple" && values.length !== 1) {
return { kind: "unusable", reason: "shape-mismatch" };
}
const matched = mapOptions(values, unit);
if (matched.kind === "ambiguous") {
return { kind: "unusable", reason: "ambiguous" };
}
if (matched.kind === "missing") {
return { kind: "unusable", reason: "shape-mismatch" };
}
const optionIds = matched.options.map((option) => option.id);
if (new Set(optionIds).size !== optionIds.length) {
return { kind: "unusable", reason: "shape-mismatch" };
}
return {
kind: "usable",
answer: {
kind: "choice",
optionIds,
displayValues: matched.options.map((option) => option.content)
}
};
}
if (unit.answerShape.kind === "matching-pair") {
if (values.length !== 1) {
return { kind: "unusable", reason: "shape-mismatch" };
}
const matched = mapOptions(values, unit);
if (matched.kind === "ambiguous") {
return { kind: "unusable", reason: "ambiguous" };
}
const option = matched.kind === "matched" ? matched.options[0] : void 0;
if (!option || !unit.answerShape.rightIds.includes(option.id)) {
return { kind: "unusable", reason: "shape-mismatch" };
}
return {
kind: "usable",
answer: {
kind: "matching-pair",
leftId: unit.answerShape.leftId,
rightId: option.id,
displayValue: option.content
}
};
}
const slotIds = unit.answerShape.slotIds;
if (unit.options.length > 0) {
if (slotIds.length !== 1 || values.length !== 1) {
return { kind: "unusable", reason: "shape-mismatch" };
}
const matched = mapOptions(values, unit);
if (matched.kind === "ambiguous") {
return { kind: "unusable", reason: "ambiguous" };
}
const option = matched.kind === "matched" ? matched.options[0] : void 0;
if (!option) return { kind: "unusable", reason: "shape-mismatch" };
return {
kind: "usable",
answer: {
kind: "slots",
slots: [{ slotId: slotIds[0], values: [option.content] }]
}
};
}
if (slotIds.length === 1) {
return {
kind: "usable",
answer: {
kind: "slots",
slots: [{ slotId: slotIds[0], values }]
}
};
}
if (values.length !== slotIds.length) {
return { kind: "unusable", reason: "shape-mismatch" };
}
return {
kind: "usable",
answer: {
kind: "slots",
slots: slotIds.map((slotId, index) => ({
slotId,
values: [values[index]]
}))
}
};
}
const displayValues = (answer) => {
switch (answer.kind) {
case "choice":
return answer.displayValues;
case "slots":
return answer.slots.flatMap((slot) => slot.values);
case "matching-pair":
return [answer.displayValue];
}
};
const transportIdempotencyKey = (unit) => `v2:${semanticContentHash(
`${unit.rootHash}
${unit.unitPath}
${unit.unitHash}`
)}`;
class AnswerSession {
constructor(adapter, client, opts = {}, deps = {}, emit = () => {
}) {
__publicField(this, "list", []);
__publicField(this, "trees", []);
__publicField(this, "currentInx", 0);
__publicField(this, "running", false);
/**
* #70:本轮 load 的收录结果。null = 收录链路没跑(adapter 不支持或没接本地存储),
* 与「跑了但收录 0 题」可区分;persisted < harvested 表示部分写入失败。
* 收录与「抓到几道可答题」解耦(纯收录页 load 返回 0),面板反馈只能从这里拿。
* `items` 带回本轮每一条收录(题干/答案/选项 + 是否落盘),面板据此列出「本页收录了什么」。
*/
__publicField(this, "lastHarvest", null);
__publicField(this, "stopFlag", false);
__publicField(this, "paidBlockReason", null);
__publicField(this, "opts");
__publicField(this, "ctx", null);
this.adapter = adapter;
this.client = client;
this.deps = deps;
this.emit = emit;
this.opts = { autoFill: true, delayMs: 1e3, freeFirst: true, ...opts };
}
setOptions(opts) {
this.opts = { ...this.opts, ...opts };
}
async loadTrees(ctx, capturedTrees) {
this.ctx = ctx;
this.paidBlockReason = null;
this.trees = capturedTrees.map((captured) => ({
captured,
answer: null,
results: /* @__PURE__ */ new Map(),
filledPaths: /* @__PURE__ */ new Set()
}));
this.list = this.trees.flatMap(
(treeState) => flattenQuestionTree(treeState.captured.root).map((unit) => {
const q = {
type: unit.queryType === "short_answer" ? "fill" : unit.queryType,
stem: unit.effectiveStem,
options: unit.options.map((option) => option.content)
};
return {
q,
status: "pending",
answer: [],
answerPlan: null,
filled: false,
charged: false,
aiGenerated: false,
free: false,
root: treeState.captured.root,
unit,
binding: treeState.captured.bindings.get(unit.unitPath),
capturedTree: treeState.captured
};
})
);
this.currentInx = 0;
this.lastHarvest = this.persistHarvested();
return this.list.length;
}
/**
* 抓题时的 DOM 是否已经整体消失(切换任务点、页面重渲染)。
* 用「全部断开」而不是「任一断开」:单题被重渲染不代表换了目标,
* 不能因此把整份会话丢掉。
*/
isStale() {
var _a2;
let total = 0;
let disconnected = 0;
for (const item of this.list) {
if (!item.capturedTree || !item.unit) continue;
total += 1;
if (!((_a2 = item.capturedTree.bindings.get(item.unit.unitPath)) == null ? void 0 : _a2.connected))
disconnected += 1;
}
return total > 0 && disconnected === total;
}
/**
* 把上一轮的答题结果搬到本轮。只在新旧题集**逐题** unitPath + unitHash 完全一致时
* 生效——超星暂存会重载题目 iframe,题目没变、只是 DOM 换了,不该把刚答完的记录冲掉;
* 换成另一份测验则一律不沿用。只搬结果数据,不碰 DOM 绑定。
*/
adoptResults(previous) {
if (previous.length === 0 || previous.length !== this.list.length)
return false;
for (const [index, item] of this.list.entries()) {
const source = previous[index];
if (!item.unit || !(source == null ? void 0 : source.unit) || source.unit.unitPath !== item.unit.unitPath || source.unit.unitHash !== item.unit.unitHash)
return false;
}
for (const [index, item] of this.list.entries()) {
const source = previous[index];
if (!source || source.status === "pending") continue;
item.status = source.status;
item.answer = source.answer;
item.answerPlan = source.answerPlan;
item.filled = source.filled;
item.charged = source.charged;
item.free = source.free;
item.aiGenerated = source.aiGenerated;
item.unsafeReason = source.unsafeReason;
if (source.answerNode) this.recordResult(item, source.answerNode);
}
return true;
}
/** 判分页收录落本地存储;键是 protocol unitHash,复用时重新过安全闸。 */
persistHarvested() {
var _a2;
const store = this.deps.localStore;
if (!this.adapter.takeHarvested) return null;
const harvested = this.adapter.takeHarvested();
if (!store) return null;
let persisted = 0;
const items = [];
for (const entry of harvested) {
let accepted = false;
try {
const meta = {
...entry.stem ? { stem: entry.stem } : {},
...entry.itemType ? { itemType: entry.itemType } : {},
...((_a2 = entry.options) == null ? void 0 : _a2.length) ? { options: entry.options } : {}
};
accepted = store.write(
entry.unitHash,
{ values: entry.values },
Object.keys(meta).length > 0 ? meta : void 0
);
if (accepted) persisted += 1;
} catch {
}
items.push({ ...entry, persisted: accepted });
}
return { harvested: harvested.length, persisted, items };
}
/**
* **只有一条路:适配器输出递归题目树。** 2026-08-20 删掉了另一条——命令式
* `capture()` + `wrapLegacyCapturedQuestions()` 包装,它唯一的实现方是同日
* 删除的 legacy `ChaoxingAdapter`。留着等于让每个读者判断「这题走的是哪条路」,
* 而答案永远是同一条。
*/
load(ctx) {
return this.adapter.captureTrees(ctx).then((trees) => this.loadTrees(ctx, trees));
}
recordResult(item, result) {
item.answerNode = result;
if (item.capturedTree) {
const treeState = this.trees.find(
(candidate) => candidate.captured === item.capturedTree
);
if (!treeState) return;
treeState.results.set(result.path, result);
treeState.answer = assembleAnswerTree(treeState.captured.root, [
...treeState.results.values()
]);
for (const candidate of this.list) {
if (candidate.capturedTree === treeState.captured) {
candidate.answerTree = treeState.answer;
candidate.treeProgress = {
hit: [...treeState.results.values()].filter(
(answer) => answer.status === "hit"
).length,
total: flattenQuestionTree(treeState.captured.root).length,
status: treeState.answer.status
};
}
}
return;
}
if (item.root) item.answerTree = assembleAnswerTree(item.root, [result]);
}
recordTerminal(item, status) {
if (!item.unit) return;
this.recordResult(item, {
kind: "leaf",
path: item.unit.unitPath,
unitHash: item.unit.unitHash,
status,
answer: null,
charged: false
});
}
async applyReadyTreePlans(capturedTree) {
if (!this.opts.autoFill || !this.ctx) return;
const treeState = this.trees.find(
(candidate) => candidate.captured === capturedTree
);
if (!(treeState == null ? void 0 : treeState.answer)) return;
const result = buildTreeFillPlans(
capturedTree.root,
treeState.answer,
capturedTree.bindings
);
if (result.blocked) {
for (const item of this.list) {
if (item.capturedTree === capturedTree && item.status === "hit" && !item.filled) {
item.unsafeReason = "atomic-tree-blocked";
}
}
return;
}
for (const plan of result.plans) {
if (treeState.filledPaths.has(plan.path)) continue;
const item = this.list.find(
(candidate) => {
var _a2;
return candidate.capturedTree === capturedTree && ((_a2 = candidate.unit) == null ? void 0 : _a2.unitPath) === plan.path;
}
);
if (!item) continue;
const filled = await this.adapter.applyTreeFillPlan(
capturedTree,
plan,
this.ctx
);
item.filled = filled;
if (filled) treeState.filledPaths.add(plan.path);
else item.unsafeReason = "adapter-rejected";
}
}
async applyHit(item, answer, options) {
item.answerPlan = answer;
item.answer = displayValues(answer);
item.status = "hit";
item.free = options.source === "free";
item.charged = options.charged;
item.aiGenerated = options.aiGenerated ?? false;
item.unsafeReason = void 0;
if (item.unit) {
this.recordResult(item, {
kind: "leaf",
path: item.unit.unitPath,
unitHash: item.unit.unitHash,
status: "hit",
answer,
source: options.source,
aiGenerated: options.aiGenerated,
charged: options.charged
});
}
if (item.capturedTree) await this.applyReadyTreePlans(item.capturedTree);
}
async persistFilledAnswers() {
if (!this.opts.autoFill || !this.ctx || !this.adapter.persistAnswers || !this.list.some((item) => item.filled))
return;
let persisted = false;
try {
persisted = await this.adapter.persistAnswers(this.ctx);
} catch {
persisted = false;
}
if (persisted) return;
for (const item of this.list) {
if (!item.filled) continue;
item.filled = false;
item.unsafeReason = "adapter-rejected";
}
for (const tree of this.trees) tree.filledPaths.clear();
}
async answerOne(inx) {
var _a2, _b, _c;
const item = this.list[inx];
if (!item || !this.ctx || !item.root || !item.unit) return;
if (item.status === "decodeFail" || item.status === "unsupported" || item.status === "hit")
return;
this.currentInx = inx;
this.emit({ kind: "question", inx });
try {
const cached = ((_a2 = this.deps.localStore) == null ? void 0 : _a2.read(item.unit.unitHash)) ?? null;
if (cached) {
const plan = buildAnswerPlan(item.unit, cached);
if (plan.kind === "usable") {
await this.applyHit(item, plan.answer, {
source: "local",
charged: false
});
return;
}
}
if (this.opts.freeFirst !== false && this.deps.freeSearch) {
try {
const freeHit = await this.deps.freeSearch(item.unit);
if (freeHit) {
const plan = buildAnswerPlan(item.unit, freeHit);
if (plan.kind === "usable") {
await this.applyHit(item, plan.answer, {
source: "free",
charged: false,
aiGenerated: freeHit.aiGenerated
});
return;
}
}
} catch {
}
}
const canPaid = this.paidBlockReason === null && (this.deps.canPaidSearch ? this.deps.canPaidSearch() : true);
if (!canPaid) {
item.status = "miss";
this.recordTerminal(
item,
this.paidBlockReason === "insufficient" ? "insufficient" : this.paidBlockReason === "ratelimited" ? "rate_limited" : "unauthorized"
);
return;
}
const request = {
requestSchemaVersion: 2,
root: item.root,
unitPath: item.unit.unitPath,
expectedRootHash: item.unit.rootHash,
expectedUnitHash: item.unit.unitHash
};
const key = this.deps.genKey ? this.deps.genKey() : transportIdempotencyKey(item.unit);
const res = await this.client.search(request, key);
if (res.code === AiAskCode.Ok && ((_b = res.result) == null ? void 0 : _b.status) === "hit") {
if (!res.result.answer) {
item.status = "unsafe";
return;
}
await this.applyHit(item, res.result.answer, {
source: res.result.source ?? "relay",
charged: res.result.charged,
aiGenerated: res.result.aiGenerated
});
} else if (res.code === AiAskCode.Ok) {
item.status = ((_c = res.result) == null ? void 0 : _c.status) === "unsafe" ? "unsafe" : "miss";
if (res.result) this.recordResult(item, res.result);
else this.recordTerminal(item, "miss");
} else if (res.code === AiAskCode.Insufficient) {
this.paidBlockReason = "insufficient";
item.status = "miss";
this.recordTerminal(item, "insufficient");
this.emit({ kind: "insufficient" });
} else if (res.code === AiAskCode.Unauthorized) {
item.status = "miss";
this.recordTerminal(item, "unauthorized");
this.emit({ kind: "unauthorized" });
} else if (res.code === AiAskCode.RateLimited) {
this.paidBlockReason = "ratelimited";
item.status = "miss";
this.recordTerminal(item, "rate_limited");
this.emit({ kind: "ratelimited" });
}
} catch (error) {
item.status = "miss";
this.recordTerminal(item, "busy");
this.emit({
kind: "search-failed",
inx,
reason: error instanceof Error ? error.message : String(error ?? "")
});
}
}
async start(fromInx = 0) {
var _a2, _b;
if (this.running) return;
this.running = true;
this.stopFlag = false;
const sleep2 = this.deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
const random = this.deps.random ?? Math.random;
try {
if (this.ctx && await ((_b = (_a2 = this.adapter).prepareStart) == null ? void 0 : _b.call(_a2, this.ctx)) === "navigating")
return;
for (let index = fromInx; index < this.list.length; index += 1) {
if (this.stopFlag) break;
if (this.isStale()) {
this.stopFlag = true;
break;
}
await this.answerOne(index);
if (this.stopFlag) break;
this.emit({ kind: "progress", inx: index, total: this.list.length });
if (index < this.list.length - 1) {
await sleep2(this.opts.delayMs + random() * 1e3);
}
}
if (this.stopFlag) this.emit({ kind: "paused" });
else {
await this.persistFilledAnswers();
this.emit({ kind: "done", ...this.stats(), total: this.list.length });
}
} finally {
this.running = false;
}
}
/**
* 无答案时随机作答。**仅单选与判断**,由调用方在整轮结束后对没填上的题逐个调用。
*
* 口径 2026-08-02 由用户放开(AGENTS §5 铁律 #6 同步改写)。**它不是安全闸的旁路**:
* 随机选中某个选项后,拿的是**那个选项自己的文本**当答案,照常走
* `buildFillPlan` → `applyAnswer`。闸要校验的「这段文本对不对得上这个选项」一步没少,
* 变的只是答案的**来源**——从题库命中换成随机挑。所以铁律 #6 的
* 「所有来源都要经过同一个安全闸」仍然成立。
*
* 拒绝的情形一律返回 false 且不改任何状态:已经填过、题型不是单选/判断、
* 没有选项、闸判 unsafe、适配器拒答。
*/
async fillRandom(inx, pick = Math.random) {
return await this.fillRandomWithReason(inx, pick) === "ok";
}
/** 同 `fillRandom`,但把**拒绝原因**说出来——排查时「为什么没填」必须可观测。 */
async fillRandomWithReason(inx, pick = Math.random) {
const item = this.list[inx];
if (!item) return "missing";
if (item.filled) return "already-filled";
if (!this.opts.autoFill) return "no-autofill";
if (!this.ctx) return "no-ctx";
if (!item.root) return "no-root";
if (!item.binding) return "no-binding";
const unit = item.unit;
if (!unit) return "no-unit";
if (unit.queryType !== "single" && unit.queryType !== "judge")
return "type-not-allowed";
const options = unit.options.filter((option) => option.content.trim());
if (options.length === 0) return "no-options";
const chosen = options[Math.floor(pick() * options.length) % options.length];
if (!chosen) return "no-options";
const fillPlan = buildFillPlan(
item.root,
unit,
{
kind: "choice",
optionIds: [chosen.id],
displayValues: [chosen.content]
},
item.binding
);
if (fillPlan.kind === "unsafe") {
item.unsafeReason = fillPlan.reason;
return "gate-unsafe";
}
const filled = item.capturedTree ? await this.adapter.applyTreeFillPlan(
item.capturedTree,
fillPlan.plan,
this.ctx
) : false;
if (!filled) {
item.unsafeReason = "adapter-rejected";
return "adapter-rejected";
}
item.filled = true;
item.random = true;
return "ok";
}
async reAnswer(inx) {
var _a2;
if (this.running) return;
const item = this.list[inx];
if (!item || !this.ctx || item.status === "decodeFail" || item.status === "unsupported")
return;
this.running = true;
this.stopFlag = false;
try {
if (item.status === "hit" && item.answerPlan) {
this.currentInx = inx;
this.emit({ kind: "question", inx });
if (!item.filled) {
await this.applyHit(item, item.answerPlan, {
source: ((_a2 = item.answerNode) == null ? void 0 : _a2.source) ?? (item.free ? "free" : "relay"),
charged: item.charged,
aiGenerated: item.aiGenerated
});
}
} else {
await this.answerOne(inx);
}
await this.persistFilledAnswers();
this.emit({ kind: "progress", inx, total: this.list.length });
} finally {
this.running = false;
}
}
resumePaidAfterCredit() {
if (this.paidBlockReason === "insufficient") this.paidBlockReason = null;
}
pause() {
this.stopFlag = true;
}
stats() {
let hit = 0;
let miss = 0;
let charged = 0;
for (const item of this.list) {
if (item.status === "hit") {
hit += 1;
if (item.charged) charged += 1;
} else if (item.status === "miss" || item.status === "unsafe") {
miss += 1;
}
}
return { hit, miss, charged };
}
}
async function withDeadline(operation, timeoutMs, label) {
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
throw new Error(`${label} timeout`);
let timer;
const deadline = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(`${label} timeout`)), timeoutMs);
});
try {
return await Promise.race([Promise.resolve().then(operation), deadline]);
} finally {
if (timer !== void 0) clearTimeout(timer);
}
}
const DEVICE_KEY_STORAGE_KEY = "aiask.security.device-key.v1";
const DEFAULT_TIMEOUT_MS$2 = 8e3;
const PRIVATE_FIELDS = ["crv", "d", "kty", "x", "y"];
const STORED_FIELDS = ["deviceId", "privateJwk", "publicJwk", "v"];
const BASE64URL_32_BYTES = /^[A-Za-z0-9_-]{43}$/u;
const KEY_SELF_TEST = utf8Bytes("aiask-device-key-self-test-v1");
const hasOnlyFields = (value, fields) => {
const actual = Object.keys(value).sort();
const expected = [...fields].sort();
return actual.length === expected.length && actual.every((field, index) => field === expected[index]);
};
function parsePrivateJwk(input) {
if (typeof input !== "object" || input === null || !hasOnlyFields(input, PRIVATE_FIELDS))
throw new Error("invalid stored device key");
const value = input;
const publicJwk2 = PublicP256JwkSchema.parse({
kty: value.kty,
crv: value.crv,
x: value.x,
y: value.y
});
if (typeof value.d !== "string" || !BASE64URL_32_BYTES.test(value.d))
throw new Error("invalid stored device key");
return { ...publicJwk2, d: value.d };
}
async function parseStoredDeviceKey(input) {
if (typeof input !== "object" || input === null || !hasOnlyFields(input, STORED_FIELDS))
throw new Error("invalid stored device key");
const value = input;
if (value.v !== 1 || typeof value.deviceId !== "string")
throw new Error("invalid stored device key");
const publicJwk2 = PublicP256JwkSchema.parse(value.publicJwk);
const privateJwk2 = parsePrivateJwk(value.privateJwk);
const deviceId = await fingerprintPublicJwk(publicJwk2);
if (deviceId !== value.deviceId)
throw new Error("invalid stored device key fingerprint");
const [privateKey, publicKey] = await Promise.all([
importEcdsaPrivateJwk(privateJwk2),
importEcdsaPublicJwk(publicJwk2)
]);
const signature2 = await signEcdsaP1363(privateKey, KEY_SELF_TEST);
if (!await verifyEcdsaP1363(publicKey, KEY_SELF_TEST, signature2))
throw new Error("invalid stored device key pair");
return { deviceId, publicJwk: publicJwk2, privateKey };
}
async function generateStoredDeviceKey(generateKeyPair) {
const pair = await generateKeyPair();
const [publicJwk2, exportedPrivateJwk] = await Promise.all([
exportPublicJwk(pair.publicKey),
exportPrivateJwk(pair.privateKey)
]);
const parsedPublicJwk = PublicP256JwkSchema.parse(publicJwk2);
const privateJwk2 = parsePrivateJwk({
...parsedPublicJwk,
d: exportedPrivateJwk.d
});
const deviceId = await fingerprintPublicJwk(parsedPublicJwk);
return {
identity: {
deviceId,
publicJwk: parsedPublicJwk,
privateKey: pair.privateKey
},
stored: { v: 1, deviceId, publicJwk: parsedPublicJwk, privateJwk: privateJwk2 }
};
}
class DeviceKeyManager {
constructor(storage, options = {}) {
__publicField(this, "timeoutMs");
__publicField(this, "generateKeyPair");
__publicField(this, "identity");
__publicField(this, "pending");
this.storage = storage;
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$2;
this.generateKeyPair = options.generateKeyPair ?? generateEcdsaDeviceKeyPair;
}
getOrCreate() {
if (this.identity) return Promise.resolve(this.identity);
if (!this.pending) {
this.track(
withDeadline(
async () => {
const stored = await this.storage.get(DEVICE_KEY_STORAGE_KEY);
if (stored !== void 0 && stored !== null)
return parseStoredDeviceKey(stored);
const generated = await generateStoredDeviceKey(
this.generateKeyPair
);
await this.storage.set(DEVICE_KEY_STORAGE_KEY, generated.stored);
return generated.identity;
},
this.timeoutMs,
"device key"
)
);
}
const pending = this.pending;
if (!pending) throw new Error("device key operation unavailable");
return pending;
}
async reset() {
if (this.pending) await this.pending.catch(() => void 0);
this.identity = void 0;
return this.track(
withDeadline(
async () => {
const generated = await generateStoredDeviceKey(this.generateKeyPair);
await this.storage.set(DEVICE_KEY_STORAGE_KEY, generated.stored);
return generated.identity;
},
this.timeoutMs,
"device key reset"
)
);
}
async clear() {
if (this.pending) await this.pending.catch(() => void 0);
await withDeadline(
() => this.storage.delete(DEVICE_KEY_STORAGE_KEY),
this.timeoutMs,
"device key clear"
);
this.identity = void 0;
}
track(operation) {
this.pending = operation.then(
(identity) => {
this.identity = identity;
this.pending = void 0;
return identity;
},
(error) => {
this.pending = void 0;
throw error;
}
);
const pending = this.pending;
if (!pending) throw new Error("device key operation unavailable");
return pending;
}
}
function createCaptchaFrameRequest(options) {
const channel = (options.channelFactory ?? (() => new MessageChannel()))();
let settled = false;
let resolveResult = () => void 0;
let rejectResult = () => void 0;
const cleanup = () => {
clearTimeout(timer);
channel.port1.onmessage = null;
channel.port1.close();
};
const fail = (reason) => {
if (settled) return;
settled = true;
cleanup();
rejectResult(new Error(reason));
};
const succeed = (token) => {
if (settled) return;
settled = true;
cleanup();
resolveResult(token);
};
const result = new Promise((resolve, reject) => {
resolveResult = resolve;
rejectResult = reject;
});
const timer = setTimeout(() => fail("challenge-timeout"), options.timeoutMs);
channel.port1.onmessage = (event) => {
const data = event.data;
if ((data == null ? void 0 : data.type) !== "aiask:captcha:result" || data.state !== options.state)
return;
if (typeof data.token === "string" && data.token.length > 0 && data.token.length <= 4096) {
succeed(data.token);
return;
}
fail(typeof data.error === "string" ? data.error : "challenge-failed");
};
channel.port1.start();
try {
options.frameWindow.postMessage(
{ type: "aiask:captcha:init", state: options.state },
options.targetOrigin,
[channel.port2]
);
} catch {
fail("challenge-unavailable");
}
return { result, cancel: () => fail("cancelled") };
}
const HIGHEST_KEYSET_VERSION_KEY = "aiask.security.highest-keyset-version.v1";
const KEYSET_WATERMARKS_KEY = "aiask.security.keyset-watermarks.v1";
const DEFAULT_TIMEOUT_MS$1 = 8e3;
const SESSION_EXPIRY_MARGIN_MS = 1e3;
function parseHighestKeysetVersion(value) {
if (value === void 0 || value === null) return 0;
if (!Number.isInteger(value) || value < 0)
throw new Error("invalid stored keyset version");
return value;
}
function keysetWatermarkOrigin(baseUrl) {
return new URL(baseUrl).origin;
}
function parseKeysetWatermarks(value) {
if (value === void 0 || value === null) return {};
if (typeof value !== "object" || Array.isArray(value))
throw new Error("invalid stored keyset watermarks");
const entries = Object.entries(value);
for (const [, version] of entries) parseHighestKeysetVersion(version);
return Object.fromEntries(entries);
}
async function readKeysetWatermark(storage, baseUrl, inheritLegacy = true) {
const origin = keysetWatermarkOrigin(baseUrl);
const own = parseKeysetWatermarks(await storage.get(KEYSET_WATERMARKS_KEY))[origin] ?? 0;
if (!inheritLegacy) return own;
const legacy = parseHighestKeysetVersion(
await storage.get(HIGHEST_KEYSET_VERSION_KEY)
);
return Math.max(own, legacy);
}
async function recordKeysetWatermark(storage, baseUrl, keysetVersion) {
const origin = keysetWatermarkOrigin(baseUrl);
const watermarks = parseKeysetWatermarks(
await storage.get(KEYSET_WATERMARKS_KEY)
);
if ((watermarks[origin] ?? 0) >= keysetVersion) return;
await storage.set(KEYSET_WATERMARKS_KEY, {
...watermarks,
[origin]: keysetVersion
});
}
const activeKey = (keyset, use, kid2, now) => keyset.keys.find(
(key) => key.kid === kid2 && key.use === use && key.notBefore <= now && key.expiresAt > now
);
const parseVersion = (value) => {
const main = value.split("-", 1)[0];
if (!/^\d+(?:\.\d+)*$/u.test(main)) throw new Error("invalid client version");
return main.split(".").map((segment) => Number(segment));
};
function compareVersions(left, right) {
const a = parseVersion(left);
const b = parseVersion(right);
const length = Math.max(a.length, b.length);
for (let index = 0; index < length; index++) {
const difference = (a[index] ?? 0) - (b[index] ?? 0);
if (difference !== 0) return difference;
}
return 0;
}
const normalizedBaseUrl$1 = (value) => value.replace(/\/+$/u, "");
class SecureSessionClient {
constructor(options) {
__publicField(this, "timeoutMs");
__publicField(this, "now");
__publicField(this, "randomBytes");
__publicField(this, "generateEcdhKeyPair");
__publicField(this, "baseUrl");
__publicField(this, "session");
__publicField(this, "pending");
this.options = options;
if (options.rootPublicJwks.length === 0)
throw new Error("missing root verification key");
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
this.now = options.now ?? (() => Date.now());
this.randomBytes = options.randomBytes ?? ((length) => crypto.getRandomValues(new Uint8Array(length)));
this.generateEcdhKeyPair = options.generateEcdhKeyPair ?? generateEcdhKeyPair;
this.baseUrl = normalizedBaseUrl$1(options.baseUrl);
}
async getSession() {
return withDeadline(
async () => {
var _a2;
const device = await this.options.deviceKeys.getOrCreate();
if (((_a2 = this.session) == null ? void 0 : _a2.deviceId) === device.deviceId && this.serverNow(this.session) + SESSION_EXPIRY_MARGIN_MS < this.session.expiresAt)
return this.session;
if (!this.pending) {
this.pending = this.openSession(device).then(
(session2) => {
this.session = session2;
this.pending = void 0;
return session2;
},
(error) => {
this.pending = void 0;
throw error;
}
);
}
const session = await this.pending;
if (session.deviceId !== device.deviceId) {
this.session = void 0;
return this.getSession();
}
return session;
},
this.timeoutMs,
"secure session"
);
}
invalidate(sessionId) {
var _a2;
if (!sessionId || ((_a2 = this.session) == null ? void 0 : _a2.sessionId) === sessionId)
this.session = void 0;
}
serverNow(session = this.session) {
return this.now() + ((session == null ? void 0 : session.serverTimeOffsetMs) ?? 0);
}
async openSession(device) {
const openPath = this.options.requestedScope === "admin" ? "/api/admin/session/open" : "/api/session/open";
const bootstrap = await this.fetchAndVerifyBootstrap();
const ephemeral = await this.generateEcdhKeyPair();
const clientEcdhPublicJwk = PublicP256JwkSchema.parse(
await exportPublicJwk(ephemeral.publicKey)
);
const nonce = this.randomBase64Url(16);
const baseRequest = {
protocolVersion: 1,
challenge: bootstrap.challenge,
deviceId: device.deviceId,
devicePublicJwk: device.publicJwk,
ecdhKid: bootstrap.ecdhKey.kid,
clientEcdhPublicJwk,
timestamp: this.now() + bootstrap.serverTimeOffsetMs,
nonce
};
const unsignedRequest = this.options.requestedScope === "admin" ? AdminSessionOpenRequestSchema.omit({ signature: true }).parse(
baseRequest
) : {
...baseRequest,
requestedScope: this.options.requestedScope
};
const request = {
...unsignedRequest,
signature: await signEcdsaP1363(
device.privateKey,
utf8Bytes(sessionOpenRequestInput(openPath, unsignedRequest))
)
};
const response = await this.send({
url: `${this.baseUrl}${openPath}`,
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
timeoutMs: this.timeoutMs
});
if (response.status < 200 || response.status >= 300)
throw new Error("session open failed");
const openResponse = SessionOpenResponseSchema.parse(
JSON.parse(response.body)
);
if (openResponse.ecdhKid !== bootstrap.ecdhKey.kid || canonicalPublicJwk(openResponse.serverEcdhPublicJwk) !== canonicalPublicJwk(bootstrap.ecdhKey.publicJwk))
throw new Error("session ECDH key mismatch");
const serverNow = this.now() + bootstrap.serverTimeOffsetMs;
const signingKey = activeKey(
bootstrap.keyset,
"transport-signing",
openResponse.signingKid,
serverNow
);
if (!signingKey) throw new Error("invalid session signing key");
const signingPublicKey = await importEcdsaPublicJwk(signingKey.publicJwk);
const { signature: _signature, ...unsignedResponse } = openResponse;
if (!await verifyEcdsaP1363(
signingPublicKey,
utf8Bytes(sessionOpenResponseInput(openPath, unsignedResponse)),
openResponse.signature
))
throw new Error("invalid session response signature");
const serverEcdhPublicKey = await importEcdhPublicJwk(
bootstrap.ecdhKey.publicJwk
);
const sharedSecret = await deriveEcdhSecret(
ephemeral.privateKey,
serverEcdhPublicKey
);
const handshakeContext = {
protocolVersion: 1,
challenge: bootstrap.challenge,
clientNonce: nonce,
serverNonce: openResponse.serverNonce,
deviceId: device.deviceId,
clientEcdhFingerprint: await fingerprintPublicJwk(clientEcdhPublicJwk),
serverEcdhFingerprint: await fingerprintPublicJwk(
bootstrap.ecdhKey.publicJwk
)
};
const handshakeKey = await deriveHandshakeKey(
sharedSecret,
handshakeContext
);
const {
ciphertext: _ciphertext,
signature: _serverSignature,
...head
} = openResponse;
const plaintext = SessionOpenPlaintextSchema.parse(
JSON.parse(
utf8Text(
await aesGcmDecrypt(
handshakeKey,
base64UrlToBytes(openResponse.iv),
base64UrlToBytes(openResponse.ciphertext),
utf8Bytes(sessionOpenResponseAad(openPath, head))
)
)
)
);
if (plaintext.deviceId !== device.deviceId)
throw new Error("session device mismatch");
if (plaintext.grantedScope !== this.options.requestedScope)
throw new Error("session scope mismatch");
if (plaintext.issuedAt > serverNow + 12e4 || plaintext.expiresAt <= serverNow || plaintext.expiresAt > bootstrap.ecdhKey.expiresAt)
throw new Error("invalid session lifetime");
const trafficKeys = await deriveTrafficKeys(sharedSecret, {
...handshakeContext,
sessionId: plaintext.sessionId
});
return {
...plaintext,
devicePublicJwk: device.publicJwk,
devicePrivateKey: device.privateKey,
serverTimeOffsetMs: bootstrap.serverTimeOffsetMs,
c2sKey: trafficKeys.c2sKey,
s2cKey: trafficKeys.s2cKey,
keyset: bootstrap.keyset
};
}
async fetchAndVerifyBootstrap() {
const response = await this.send({
url: `${this.baseUrl}/api/bootstrap`,
method: "GET",
timeoutMs: this.timeoutMs
});
if (response.status < 200 || response.status >= 300)
throw new Error("bootstrap failed");
const document2 = BootstrapDocumentSchema.parse(JSON.parse(response.body));
let rootVerified = false;
for (const rootJwk of this.options.rootPublicJwks) {
const rootPublicKey = await importEcdsaPublicJwk(rootJwk);
if (await verifyServerKeysetSignature(rootPublicKey, document2.keyset)) {
rootVerified = true;
break;
}
}
if (!rootVerified) throw new Error("invalid keyset root signature");
if (await serverKeysetHash(document2.keyset) !== document2.challenge.keysetHash)
throw new Error("keyset hash mismatch");
if (!await verifyBootstrapChallengeSignature(
document2.keyset,
document2.challenge
))
throw new Error("invalid bootstrap challenge signature");
const localNow = this.now();
const serverNow = document2.challenge.serverTime;
const highestAcceptedVersion = await this.readHighestKeysetVersion();
validateServerKeyset(document2.keyset, serverNow, highestAcceptedVersion);
validateBootstrapChallenge(document2.challenge, document2.keyset, serverNow);
if (compareVersions(
this.options.clientVersion,
document2.challenge.minClientVersion
) < 0)
throw new Error("client version too old");
const ecdhKey = document2.keyset.keys.find(
(key) => key.use === "ecdh" && key.notBefore <= serverNow && key.expiresAt > serverNow
);
if (!ecdhKey) throw new Error("missing active ECDH key");
await recordKeysetWatermark(
this.options.stateStorage,
this.baseUrl,
document2.keyset.keysetVersion
);
return {
keyset: document2.keyset,
challenge: document2.challenge.challenge,
serverTimeOffsetMs: serverNow - localNow,
ecdhKey
};
}
async readHighestKeysetVersion() {
return readKeysetWatermark(
this.options.stateStorage,
this.baseUrl,
this.options.inheritLegacyKeysetWatermark
);
}
randomBase64Url(length) {
const bytes = this.randomBytes(length);
if (!(bytes instanceof Uint8Array) || bytes.length !== length)
throw new Error("invalid random source");
return bytesToBase64Url(bytes);
}
send(request) {
return withDeadline(
() => this.options.transport.send(request),
request.timeoutMs ?? this.timeoutMs,
"security transport"
);
}
}
const DEFAULT_TIMEOUT_MS = 8e3;
const DEFAULT_MAX_ATTEMPTS = 2;
const RESPONSE_TIME_WINDOW_MS = 12e4;
class RetryableSecureTransportError extends Error {
}
const headerEntries = (headers, name) => Object.entries(headers ?? {}).filter(([key]) => key.toLowerCase() === name.toLowerCase()).map(([, value]) => value);
function singleHeader(headers, name) {
const values = headerEntries(headers, name);
if (values.length > 1) throw new Error(`duplicate ${name} header`);
return values[0];
}
class SecureTransport {
constructor(options) {
__publicField(this, "timeoutMs");
__publicField(this, "maxAttempts");
__publicField(this, "randomBytes");
__publicField(this, "randomUuid");
this.options = options;
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
if (!Number.isInteger(this.maxAttempts) || this.maxAttempts < 1)
throw new Error("invalid secure transport attempts");
this.randomBytes = options.randomBytes ?? ((length) => crypto.getRandomValues(new Uint8Array(length)));
this.randomUuid = options.randomUuid ?? (() => crypto.randomUUID());
}
async send(request) {
if (headerEntries(request.headers, "Authorization").length > 0)
throw new Error("Authorization header is forbidden on SecureTransport");
const idempotencyKey = singleHeader(request.headers, "Idempotency-Key");
const url = new URL(request.url);
if (url.username || url.password || url.search)
throw new Error(
"secure request URL must not contain credentials or query"
);
const payload = request.body === void 0 ? null : JSON.parse(request.body);
const accessToken = this.options.getAccessToken ? await withDeadline(
() => {
var _a2, _b;
return Promise.resolve(((_b = (_a2 = this.options).getAccessToken) == null ? void 0 : _b.call(_a2)) ?? "");
},
request.timeoutMs ?? this.timeoutMs,
"access token"
) : "";
const plaintext = {
...accessToken ? { auth: { accessToken } } : {},
...idempotencyKey ? { idempotencyKey } : {},
payload
};
const attempts = idempotencyKey ? this.maxAttempts : 1;
let lastError;
for (let attempt = 0; attempt < attempts; attempt++) {
try {
return await this.sendAttempt(request, url.pathname, plaintext);
} catch (error) {
lastError = error;
if (!(error instanceof RetryableSecureTransportError)) throw error;
}
}
throw lastError;
}
async sendAttempt(request, path, plaintext) {
const timeoutMs = request.timeoutMs ?? this.timeoutMs;
const session = await this.options.sessions.getSession();
const iv = this.randomBase64Url(12);
const requestHead = {
v: 1,
sessionId: session.sessionId,
requestId: this.randomUuid(),
timestamp: this.options.sessions.serverNow(session),
nonce: this.randomBase64Url(16),
iv
};
const ciphertext = bytesToBase64Url(
await aesGcmEncrypt(
session.c2sKey,
base64UrlToBytes(iv),
utf8Bytes(JSON.stringify(plaintext)),
utf8Bytes(requestEnvelopeAad(request.method, path, requestHead))
)
);
const unsignedEnvelope = { ...requestHead, ciphertext };
const envelope = {
...unsignedEnvelope,
signature: await signEcdsaP1363(
session.devicePrivateKey,
utf8Bytes(requestEnvelopeInput(request.method, path, unsignedEnvelope))
)
};
let response;
try {
response = await withDeadline(
() => this.options.transport.send({
url: request.url,
method: request.method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(envelope),
timeoutMs
}),
timeoutMs,
"secure request"
);
} catch (error) {
throw new RetryableSecureTransportError(
error instanceof Error ? error.message : "secure request failed"
);
}
if (!response.body) {
if (response.status === 401 || response.status === 409) {
this.options.sessions.invalidate(session.sessionId);
throw new RetryableSecureTransportError("secure session rejected");
}
if (response.status >= 200 && response.status < 300)
throw new Error("missing secure response");
return response;
}
const responseEnvelope = SecureResponseEnvelopeSchema.parse(
JSON.parse(response.body)
);
if (responseEnvelope.sessionId !== session.sessionId || responseEnvelope.requestId !== requestHead.requestId)
throw new Error("secure response correlation mismatch");
const serverNow = this.options.sessions.serverNow(session);
if (Math.abs(responseEnvelope.timestamp - serverNow) > RESPONSE_TIME_WINDOW_MS)
throw new Error("secure response timestamp rejected");
const signingKey = session.keyset.keys.find(
(key) => key.kid === responseEnvelope.kid && key.use === "transport-signing" && key.notBefore <= serverNow && key.expiresAt > serverNow
);
if (!signingKey) throw new Error("secure response signing key rejected");
const publicKey = await importEcdsaPublicJwk(signingKey.publicJwk);
const { signature: _signature, ...unsignedResponse } = responseEnvelope;
if (!await verifyEcdsaP1363(
publicKey,
utf8Bytes(
responseEnvelopeInput(request.method, path, unsignedResponse)
),
responseEnvelope.signature
))
throw new Error("secure response signature rejected");
const { ciphertext: _ciphertext, ...responseHead } = unsignedResponse;
const body = utf8Text(
await aesGcmDecrypt(
session.s2cKey,
base64UrlToBytes(responseEnvelope.iv),
base64UrlToBytes(responseEnvelope.ciphertext),
utf8Bytes(responseEnvelopeAad(request.method, path, responseHead))
)
);
return { status: response.status, body };
}
randomBase64Url(length) {
const bytes = this.randomBytes(length);
if (!(bytes instanceof Uint8Array) || bytes.length !== length)
throw new Error("invalid random source");
return bytesToBase64Url(bytes);
}
}
class RuleExecutionError extends Error {
constructor(code, message) {
super(message);
__publicField(this, "fatal", true);
this.code = code;
this.name = "RuleExecutionError";
}
}
class RuleDomainError extends Error {
constructor(code, message) {
super(message);
this.code = code;
this.name = "RuleDomainError";
}
toRuleValue() {
return { code: this.code, message: this.message };
}
}
class PrimitiveRegistry {
constructor() {
__publicField(this, "definitions", /* @__PURE__ */ new Map());
}
register(definition) {
if (this.definitions.has(definition.id)) {
throw new Error(`duplicate primitive: ${definition.id}`);
}
this.definitions.set(definition.id, definition);
}
get(id) {
return this.definitions.get(id);
}
/** 已注册原语 id;用于校验 policy 与运行时注册表不漂移。 */
ids() {
return [...this.definitions.keys()];
}
}
function assertPrimitiveAllowed(registry, policy, invocation) {
if (!policy.primitives.has(invocation.id)) {
throw new RuleExecutionError(
"capability_denied",
`primitive is not allowed by runtime policy: ${invocation.id}`
);
}
const definition = registry.get(invocation.id);
if (!definition) {
throw new RuleExecutionError(
"unknown_primitive",
`unknown primitive: ${invocation.id}`
);
}
if (!definition.phases.includes(invocation.phase)) {
throw new RuleExecutionError(
"capability_denied",
`primitive is not allowed in ${invocation.phase}: ${invocation.id}`
);
}
if (definition.capability && (!policy.capabilities.has(definition.capability) || !invocation.requestedCapabilities.has(definition.capability))) {
throw new RuleExecutionError(
"capability_denied",
`capability is not allowed: ${definition.capability}`
);
}
if (definition.requiresSafetyCapability) {
const argument = definition.safetyArgument ?? "safety";
try {
assertSafetyCapability(invocation.args[argument]);
} catch {
throw new RuleExecutionError(
"security_violation",
`primitive requires a valid safety capability: ${invocation.id}`
);
}
}
return definition;
}
async function invokePrimitive(definition, invocation) {
try {
return await definition.execute({
args: invocation.args,
phase: invocation.phase,
signal: invocation.signal,
variables: invocation.variables
});
} catch (error) {
if (error instanceof RuleExecutionError || error instanceof RuleDomainError)
throw error;
throw new RuleDomainError(
"primitive_failed",
error instanceof Error ? error.message : "primitive failed"
);
}
}
function safetyArgument(args) {
return args.safety;
}
function stringArgument$2(args, name) {
const value = args[name];
if (typeof value !== "string" || value.length === 0)
throw new RuleDomainError("invalid_type", `${name} must be a string`);
return value;
}
function assertAllowedOperation(capability, operation) {
const plan = safetyPlanForCapability(capability);
if (plan.atomic && plan.operations.length > 1)
throw new RuleDomainError(
"partial_not_allowed",
"atomic fill plan cannot be split into individual writes"
);
try {
assertSafetyOperation(capability, operation);
} catch {
throw new RuleExecutionError(
"security_violation",
"answer operation is not allowed by safety capability"
);
}
return plan;
}
async function applyAndVerifyOperation(writer, capability, operation, signal) {
const plan = assertAllowedOperation(capability, operation);
if (!await writer.applyOperation(plan, operation, signal))
throw new RuleDomainError(
"write_refused",
"answer writer refused operation"
);
if (!await writer.verifyOperation(plan, operation, signal))
throw new RuleDomainError(
"write_verify_failed",
"answer write read-back verification failed"
);
return true;
}
function registerAnswerWritePrimitives(registry, writer) {
registry.register({
id: "answer.applyPlan",
phases: ["fill"],
capability: "answer-write",
requiresSafetyCapability: true,
execute: async ({ args, signal }) => {
const plan = safetyPlanForCapability(safetyArgument(args));
if (!await writer.applyPlan(plan, signal))
throw new RuleDomainError("write_refused", "answer writer refused plan");
if (!await writer.verifyPlan(plan, signal))
throw new RuleDomainError(
"write_verify_failed",
"answer plan read-back verification failed"
);
return true;
}
});
const registerChoice = (id) => registry.register({
id,
phases: ["fill"],
capability: "answer-write",
requiresSafetyCapability: true,
execute: ({ args, signal }) => {
if (id === "dom.setChecked" && args.checked !== true)
throw new RuleExecutionError(
"security_violation",
"dom.setChecked only accepts checked=true for planned answers"
);
return applyAndVerifyOperation(
writer,
safetyArgument(args),
{ kind: "choose", optionId: stringArgument$2(args, "optionId") },
signal
);
}
});
registerChoice("dom.clickAnswer");
registerChoice("dom.setChecked");
const registerSlot = (id) => registry.register({
id,
phases: ["fill"],
capability: "answer-write",
requiresSafetyCapability: true,
execute: ({ args, signal }) => applyAndVerifyOperation(
writer,
safetyArgument(args),
{
kind: "write",
slotId: stringArgument$2(args, "slotId"),
value: stringArgument$2(args, "value")
},
signal
)
});
registerSlot("dom.setValue");
registerSlot("dom.setSelected");
registry.register({
id: "matching.pair",
phases: ["fill"],
capability: "answer-write",
requiresSafetyCapability: true,
execute: ({ args, signal }) => applyAndVerifyOperation(
writer,
safetyArgument(args),
{
kind: "pair",
leftId: stringArgument$2(args, "leftId"),
rightId: stringArgument$2(args, "rightId")
},
signal
)
});
}
const MAX_HARD_TREES = 5e3;
const MAX_HARD_BINDINGS = 5e3;
class RuleCaptureRegistryError extends Error {
constructor(code, message) {
super(message);
this.code = code;
this.name = "RuleCaptureRegistryError";
}
}
function freezeJson$1(value) {
if (!value || typeof value !== "object" || Object.isFrozen(value))
return value;
for (const child of Object.values(value)) freezeJson$1(child);
return Object.freeze(value);
}
class RuleCaptureRegistry {
constructor(options) {
__publicField(this, "bindings");
__publicField(this, "trees", []);
__publicField(this, "pendingBindings", /* @__PURE__ */ new Map());
/** 页面收录:按 protocol unitHash 归并,与可答树完全解耦。 */
__publicField(this, "harvested", /* @__PURE__ */ new Map());
__publicField(this, "finishedResults", /* @__PURE__ */ new WeakSet());
__publicField(this, "maxTrees");
__publicField(this, "maxBindings");
__publicField(this, "finished", false);
__publicField(this, "disposed", false);
const maxBindings = options.maxBindings ?? MAX_HARD_BINDINGS;
if (!Number.isInteger(options.maxTrees) || options.maxTrees <= 0 || options.maxTrees > MAX_HARD_TREES || !Number.isInteger(maxBindings) || maxBindings <= 0 || maxBindings > MAX_HARD_BINDINGS)
throw new RuleCaptureRegistryError(
"invalid_options",
"invalid capture tree limit"
);
this.maxTrees = options.maxTrees;
this.maxBindings = maxBindings;
this.bindings = new RuleBindingRegistry({ maxBindings: this.maxBindings });
}
registerLeafBinding(input, registration) {
if (this.finished || this.disposed)
throw new RuleCaptureRegistryError(
"finished",
"capture registry is finished"
);
const parsed = LeafQuestionNodeSchema.safeParse(input);
if (!parsed.success)
throw new RuleCaptureRegistryError(
"invalid_question_node",
"capture node is not a valid leaf question"
);
const node = freezeJson$1(parsed.data);
if (this.pendingBindings.has(node.path) || this.bindings.has(node.path))
throw new RuleCaptureRegistryError(
"binding_registration",
`duplicate binding path: ${node.path}`
);
if (this.bindings.size + this.pendingBindings.size >= this.maxBindings)
throw new RuleCaptureRegistryError(
"binding_registration",
"binding registry limit exceeded"
);
const pending = {
path: node.path,
capturedFingerprint: questionNodeHash(node),
readCurrentFingerprint: registration.readCurrentFingerprint,
targets: registration.targets
};
const validation = new RuleBindingRegistry({ maxBindings: 1 });
try {
validation.register(pending);
} catch (error) {
if (error instanceof RuleBindingRegistryError)
throw new RuleCaptureRegistryError(
"binding_registration",
error.message
);
throw error;
} finally {
validation.dispose();
}
this.pendingBindings.set(node.path, pending);
return node.path;
}
/**
* 登记判分页展示的正确答案(收录)。只吃题目节点本身,自己算 unitHash,
* 不需要可写 binding——判分页没有可作答元素。
* 值是原始展示文本;复用时仍要过 buildAnswerPlan 和 SafetyGate。
*/
registerHarvestLeaf(input, values) {
if (this.finished || this.disposed)
throw new RuleCaptureRegistryError(
"finished",
"capture registry is finished"
);
const parsed = LeafQuestionNodeSchema.safeParse(input);
if (!parsed.success)
throw new RuleCaptureRegistryError(
"invalid_question_node",
"harvest node is not a valid leaf question"
);
const units = flattenQuestionTree(freezeJson$1(parsed.data));
const unit = units[0];
if (units.length !== 1 || !unit)
throw new RuleCaptureRegistryError(
"harvest_registration",
"harvest node must produce exactly one query unit"
);
if (!Array.isArray(values) || values.length === 0)
throw new RuleCaptureRegistryError(
"harvest_registration",
"harvest requires a non-empty value array"
);
const normalized = values.map((value) => {
if (typeof value !== "string" || !value.trim())
throw new RuleCaptureRegistryError(
"harvest_registration",
"harvest values must be non-blank strings"
);
return value.trim();
});
if (this.harvested.size >= this.maxBindings && !this.harvested.has(unit.unitHash))
throw new RuleCaptureRegistryError(
"harvest_registration",
"harvest registry limit exceeded"
);
this.harvested.set(unit.unitHash, {
values: normalized,
stem: unit.effectiveStem,
itemType: unit.queryType,
options: unit.options.map((option) => option.content)
});
return unit.unitHash;
}
/** 取走本次收录结果;调用后清空,避免跨页重复写入。 */
takeHarvested() {
const result = [...this.harvested].map(([unitHash, entry]) => ({
unitHash,
values: [...entry.values],
stem: entry.stem,
itemType: entry.itemType,
options: [...entry.options]
}));
this.harvested.clear();
return result;
}
registerTree(input) {
if (this.finished || this.disposed)
throw new RuleCaptureRegistryError(
"finished",
"capture registry is finished"
);
if (this.trees.length >= this.maxTrees)
throw new RuleCaptureRegistryError(
"tree_limit",
"capture tree limit exceeded"
);
const parsed = QuestionNodeSchema.safeParse(input);
if (!parsed.success)
throw new RuleCaptureRegistryError(
"invalid_question_node",
"capture root is not a valid question tree"
);
try {
assertValidQuestionTree(parsed.data);
} catch (error) {
throw new RuleCaptureRegistryError(
"invalid_question_node",
error instanceof Error ? error.message : "invalid question tree"
);
}
const root = freezeJson$1(parsed.data);
const units = flattenQuestionTree(root);
try {
const pending = units.map((unit) => {
const binding = this.pendingBindings.get(unit.unitPath);
if (!binding || binding.capturedFingerprint !== unit.sourceNodeHash)
throw new RuleCaptureRegistryError(
"binding_registration",
`missing or stale binding for query unit: ${unit.unitPath}`
);
if (this.bindings.has(unit.unitPath))
throw new RuleCaptureRegistryError(
"binding_registration",
`duplicate binding path: ${unit.unitPath}`
);
return binding;
});
if (this.bindings.size + pending.length > this.maxBindings)
throw new RuleCaptureRegistryError(
"binding_registration",
"binding registry limit exceeded"
);
for (const binding of pending) this.bindings.register(binding);
for (const binding of pending) this.pendingBindings.delete(binding.path);
} catch (error) {
for (const unit of units) this.pendingBindings.delete(unit.unitPath);
if (error instanceof RuleBindingRegistryError)
throw new RuleCaptureRegistryError(
"binding_registration",
error.message
);
throw error;
}
const tree = Object.freeze({
root,
bindings: this.bindings
});
this.trees.push(tree);
return tree;
}
registerLeaf(input, registration) {
if (this.finished || this.disposed)
throw new RuleCaptureRegistryError(
"finished",
"capture registry is finished"
);
if (this.trees.length >= this.maxTrees)
throw new RuleCaptureRegistryError(
"tree_limit",
"capture tree limit exceeded"
);
this.registerLeafBinding(input, registration);
return this.registerTree(input);
}
finish() {
if (this.disposed) return [];
if (this.pendingBindings.size > 0)
throw new RuleCaptureRegistryError(
"binding_registration",
"capture has uncommitted question bindings"
);
if (!this.finished) {
this.bindings.seal();
this.finished = true;
}
const result = [...this.trees];
this.finishedResults.add(result);
return result;
}
ownsFinishedResult(value) {
return Array.isArray(value) && this.finished && !this.disposed && this.finishedResults.has(value);
}
dispose() {
this.disposed = true;
this.finished = true;
this.trees.length = 0;
this.pendingBindings.clear();
this.harvested.clear();
this.bindings.dispose();
}
}
function writeText(el, value) {
var _a2;
if (!el.isConnected) return false;
const win = el.ownerDocument.defaultView;
if (!win) return false;
const Ctor = el.tagName === "TEXTAREA" ? win.HTMLTextAreaElement : win.HTMLInputElement;
const setter = (_a2 = Object.getOwnPropertyDescriptor(Ctor.prototype, "value")) == null ? void 0 : _a2.set;
el.focus();
if (setter) setter.call(el, value);
else el.value = value;
el.dispatchEvent(new win.Event("input", { bubbles: true }));
el.dispatchEvent(new win.Event("change", { bubbles: true }));
el.blur();
return true;
}
function validId(value) {
return value.length > 0 && value.length <= 256;
}
function assertActive$2(signal) {
if (signal.aborted)
throw new RuleExecutionError("cancelled", "DOM answer target was cancelled");
}
function isLiveElement(element) {
return element.isConnected && element.ownerDocument.defaultView !== null;
}
function htmlElement(element, label) {
const view = element.ownerDocument.defaultView;
if (!view || !(element instanceof view.HTMLElement))
throw new RuleExecutionError(
"security_violation",
`${label} must be an HTMLElement`
);
return element;
}
function assertSafeChoiceTarget(element, location2, depth = 0) {
const target = htmlElement(element, "choice target");
const view = target.ownerDocument.defaultView;
if (!view)
throw new RuleExecutionError(
"security_violation",
"choice target has no window"
);
if (target instanceof view.HTMLFormElement)
throw new RuleExecutionError(
"security_violation",
"choice target cannot be a submit form"
);
if (target instanceof view.HTMLButtonElement && target.type !== "button")
throw new RuleExecutionError(
"security_violation",
"choice target cannot be a submit button"
);
if (target instanceof view.HTMLInputElement && !["radio", "checkbox"].includes(target.type))
throw new RuleExecutionError(
"security_violation",
"choice target input type must be radio or checkbox"
);
if (target instanceof view.HTMLAnchorElement) {
const url = new URL(target.href, location2.href);
if (url.protocol === "javascript:" || url.origin !== location2.origin)
throw new RuleExecutionError(
"security_violation",
"choice target cannot navigate outside the current origin"
);
throw new RuleExecutionError(
"security_violation",
"choice target cannot be a navigation link"
);
}
if (/提交|交卷|完成考试|\bsubmit\b/iu.test(target.textContent ?? ""))
throw new RuleExecutionError(
"security_violation",
"choice target cannot activate submit controls"
);
if (target instanceof view.HTMLLabelElement && depth === 0) {
const control = target.control;
if (control) assertSafeChoiceTarget(control, location2, depth + 1);
}
return target;
}
function checkedInput(element) {
const view = element.ownerDocument.defaultView;
const candidate = view && element instanceof view.HTMLInputElement ? element : element.querySelector('input[type="radio"], input[type="checkbox"]');
if (!view || !candidate || !(candidate instanceof view.HTMLInputElement) || !["radio", "checkbox"].includes(candidate.type))
throw new RuleExecutionError(
"security_violation",
"checked strategy requires a radio or checkbox input"
);
return candidate;
}
function selectedReader(target, strategy) {
if (strategy.kind === "checked") {
checkedInput(target);
return () => {
try {
return checkedInput(target).checked;
} catch {
return false;
}
};
}
if (strategy.kind === "class") {
if (!strategy.name || strategy.name.length > 128 || /\s/u.test(strategy.name))
throw new RuleExecutionError(
"security_violation",
"invalid selected class name"
);
return () => target.classList.contains(strategy.name);
}
if (strategy.kind === "descendant") {
if (!strategy.selector || strategy.selector.length > 1024)
throw new RuleExecutionError(
"security_violation",
"invalid selected descendant selector"
);
try {
target.querySelector(strategy.selector);
} catch {
throw new RuleExecutionError(
"security_violation",
"invalid selected descendant selector"
);
}
return () => target.querySelector(strategy.selector) != null;
}
if (!/^[A-Za-z_:][A-Za-z0-9_.:-]*$/u.test(strategy.name) || /^on/iu.test(strategy.name) || strategy.value.length > 1024)
throw new RuleExecutionError(
"security_violation",
"invalid selected attribute strategy"
);
return () => target.getAttribute(strategy.name) === strategy.value;
}
function createDomChoiceBindingTarget(options) {
if (!validId(options.optionId))
throw new RuleExecutionError("security_violation", "invalid option id");
const clickTarget = assertSafeChoiceTarget(
options.clickTarget,
options.location
);
const readTarget = options.readTarget ?? clickTarget;
const isSelected = selectedReader(readTarget, options.selected);
let clicked = false;
return {
kind: "choose",
optionId: options.optionId,
isConnected: () => isLiveElement(clickTarget) && isLiveElement(readTarget),
apply: async (operation, signal) => {
assertActive$2(signal);
if (operation.kind !== "choose" || operation.optionId !== options.optionId || !isLiveElement(clickTarget) || !isLiveElement(readTarget))
return false;
if (isSelected()) return true;
clickTarget.click();
clicked = true;
return true;
},
verify: async (operation, signal) => {
assertActive$2(signal);
return operation.kind === "choose" && operation.optionId === options.optionId && isLiveElement(clickTarget) && isLiveElement(readTarget) && isSelected();
},
// 复选框再点一次就是取消,和页面自己的事件流完全一致;radio 点不回去,
// 直接改 .checked 会绕开页面的 change 处理让 SPA 状态错乱,宁可如实说撤不掉。
revert: async (signal) => {
assertActive$2(signal);
if (!clicked) return true;
if (!isLiveElement(clickTarget) || !isLiveElement(readTarget))
return false;
if (!isCheckboxControl(clickTarget)) return false;
if (isSelected()) clickTarget.click();
clicked = false;
return !isSelected();
}
};
}
function isCheckboxControl(target) {
const view = target.ownerDocument.defaultView;
if (!view) return false;
const control = target instanceof view.HTMLLabelElement ? target.control : target;
return control instanceof view.HTMLInputElement && control.type === "checkbox";
}
function writeEvents(element) {
const view = element.ownerDocument.defaultView;
if (!view) return;
element.dispatchEvent(new view.Event("input", { bubbles: true }));
element.dispatchEvent(new view.Event("change", { bubbles: true }));
}
function writeSelect(element, value) {
var _a2;
if (!isLiveElement(element)) return false;
if (![...element.options].some((option) => option.value === value))
return false;
const view = element.ownerDocument.defaultView;
if (!view) return false;
const setter = (_a2 = Object.getOwnPropertyDescriptor(
view.HTMLSelectElement.prototype,
"value"
)) == null ? void 0 : _a2.set;
element.focus();
if (setter) setter.call(element, value);
else element.value = value;
writeEvents(element);
element.blur();
return true;
}
function normalizeSelectContent(value) {
return value.replace(/\s+/gu, " ").trim();
}
function selectOptionMaps(select, options) {
var _a2;
if (options == null) return null;
if (options.length === 0)
throw new RuleExecutionError(
"security_violation",
"select option mapping cannot be empty"
);
const valueByContent = /* @__PURE__ */ new Map();
const contentByValue = /* @__PURE__ */ new Map();
for (const option of options) {
if (!validId(option.id) || !option.content.trim() || valueByContent.has(option.content) || contentByValue.has(option.id))
throw new RuleExecutionError(
"security_violation",
"select option mapping is ambiguous"
);
const domMatches = [...select.options].filter(
(candidate) => candidate.value === option.id
);
if (domMatches.length !== 1 || normalizeSelectContent(((_a2 = domMatches[0]) == null ? void 0 : _a2.textContent) ?? "") !== normalizeSelectContent(option.content))
throw new RuleExecutionError(
"security_violation",
"select option mapping does not match DOM"
);
valueByContent.set(option.content, option.id);
contentByValue.set(option.id, option.content);
}
return { valueByContent, contentByValue };
}
function contentEditable(element) {
var _a2;
const value = (_a2 = element.getAttribute("contenteditable")) == null ? void 0 : _a2.toLowerCase();
return value === "" || value === "true" || value === "plaintext-only";
}
function createDomWriteBindingTarget(options) {
if (!validId(options.slotId))
throw new RuleExecutionError("security_violation", "invalid slot id");
const element = htmlElement(options.element, "write target");
const view = element.ownerDocument.defaultView;
if (!view)
throw new RuleExecutionError(
"security_violation",
"write target has no window"
);
const input = element instanceof view.HTMLInputElement ? element : void 0;
const textarea = element instanceof view.HTMLTextAreaElement ? element : void 0;
const select = element instanceof view.HTMLSelectElement ? element : void 0;
const editor = !input && !textarea && !select && contentEditable(element);
if (options.selectOptions && !select)
throw new RuleExecutionError(
"security_violation",
"select option mapping requires a select target"
);
if (input && !["text", "search", "tel", "url", "email", "number"].includes(input.type))
throw new RuleExecutionError(
"security_violation",
`write target input type is not allowed: ${input.type}`
);
if (!input && !textarea && !select && !editor)
throw new RuleExecutionError(
"security_violation",
"write target must be a text input, textarea, select, or contenteditable"
);
const selectMaps = select ? selectOptionMaps(select, options.selectOptions) : null;
const write = (value) => {
if (input || textarea)
return writeText(input ?? textarea, value);
if (select) {
const selectValue = (selectMaps == null ? void 0 : selectMaps.valueByContent.get(value)) ?? value;
if (selectMaps && !selectMaps.valueByContent.has(value)) return false;
return writeSelect(select, selectValue);
}
if (!isLiveElement(element)) return false;
element.focus();
element.textContent = value;
writeEvents(element);
element.blur();
return true;
};
const read = () => {
var _a2;
if (input || textarea) return ((_a2 = input ?? textarea) == null ? void 0 : _a2.value) ?? "";
if (select)
return (selectMaps == null ? void 0 : selectMaps.contentByValue.get(select.value)) ?? select.value;
return element.textContent ?? "";
};
let previous = null;
return {
kind: "write",
slotId: options.slotId,
isConnected: () => isLiveElement(element),
apply: async (operation, signal) => {
assertActive$2(signal);
if (operation.kind !== "write" || operation.slotId !== options.slotId || !isLiveElement(element))
return false;
const before = read();
if (!write(operation.value)) return false;
previous ?? (previous = before);
return true;
},
revert: async (signal) => {
assertActive$2(signal);
if (previous == null) return true;
if (!isLiveElement(element)) return false;
const restored = write(previous);
if (restored) previous = null;
return restored;
},
verify: async (operation, signal) => {
assertActive$2(signal);
return operation.kind === "write" && operation.slotId === options.slotId && isLiveElement(element) && read() === operation.value;
}
};
}
const MAX_TARGETS = 256;
function domArgument$1(args, name, refs) {
return refs.getDom(args[name]);
}
function domArrayArgument(args, name, refs) {
const value = args[name];
if (value == null) return [];
if (!Array.isArray(value) || value.length > MAX_TARGETS)
throw new RuleDomainError(
"invalid_type",
`${name} must be a bounded DOM reference array`
);
return value.map((item) => refs.getDom(item));
}
function stringArrayArgument$1(args, name) {
const value = args[name];
if (value == null) return [];
if (!Array.isArray(value) || value.length > 64)
throw new RuleDomainError(
"invalid_type",
`${name} must be a bounded string array`
);
return value.map((item) => {
if (typeof item !== "string" || !item || item.length > 1024)
throw new RuleDomainError(
"invalid_type",
`${name} contains an invalid selector`
);
return item;
});
}
function optionalString(args, name, maxLength = 1024) {
const value = args[name];
if (value == null) return void 0;
if (typeof value !== "string" || value.length === 0 || value.length > maxLength)
throw new RuleDomainError(
"invalid_type",
`${name} must be a bounded string`
);
return value;
}
function parseLeafNode(value) {
const parsed = LeafQuestionNodeSchema.safeParse(value);
if (!parsed.success)
throw new RuleDomainError(
"invalid_question_node",
"capture node is not a valid leaf question"
);
return parsed.data;
}
function selectedStrategy(args) {
const kind = args.selectedBy;
if (kind === "checked") return { kind };
if (kind === "class") {
const name = optionalString(args, "selectedClass", 128);
if (!name)
throw new RuleDomainError(
"invalid_type",
"selectedClass is required for class strategy"
);
return { kind, name };
}
if (kind === "attribute") {
const name = optionalString(args, "selectedAttribute", 128);
const value = optionalString(args, "selectedValue");
if (!name || value == null)
throw new RuleDomainError(
"invalid_type",
"selectedAttribute and selectedValue are required"
);
return { kind, name, value };
}
if (kind === "descendant") {
const selector = optionalString(args, "selectedDescendantSelector");
if (!selector)
throw new RuleDomainError(
"invalid_type",
"selectedDescendantSelector is required for descendant strategy"
);
return { kind, selector };
}
throw new RuleDomainError(
"invalid_type",
"selectedBy must be checked, class, descendant, or attribute"
);
}
function queryReadTarget(element, selector) {
if (!selector) return element;
try {
const target = element.querySelector(selector);
if (!target)
throw new RuleDomainError(
"binding_target_mismatch",
`choice read target is missing: ${selector}`
);
return target;
} catch (error) {
if (error instanceof RuleDomainError) throw error;
throw new RuleDomainError(
"invalid_selector",
error instanceof Error ? error.message : "invalid read selector"
);
}
}
function position(element) {
return element.parentElement ? Array.from(element.parentElement.children).indexOf(element) : -1;
}
function targetDescriptor(element) {
const view = element.ownerDocument.defaultView;
const input = view && element instanceof view.HTMLInputElement ? element : void 0;
const select = view && element instanceof view.HTMLSelectElement ? element : void 0;
return [
element.tagName.toLowerCase(),
(input == null ? void 0 : input.type) ?? "",
element.getAttribute("name") ?? "",
element.id,
position(element),
select ? [...select.options].map((option) => [option.value, option.textContent]) : []
];
}
function domFingerprint(source) {
const allTargets = [
source.stem,
...source.contentTargets,
...source.answerTargets
];
if (allTargets.some((target) => !target.isConnected))
throw new Error("binding fingerprint target is disconnected");
return semanticContentHash(
JSON.stringify([
serializeDomQuestionContent(source.stem, {
stripSelectors: source.stemStripSelectors
}),
source.contentTargets.map(
(target) => serializeDomQuestionContent(target, {
stripSelectors: source.optionStripSelectors
})
),
source.answerTargets.map(targetDescriptor)
])
);
}
function leafDomRegistration(node, args, environment) {
const stem = domArgument$1(args, "stemTarget", environment.refs);
const stemStripSelectors = stringArrayArgument$1(args, "stemStripSelectors");
const optionStripSelectors = stringArrayArgument$1(args, "optionStripSelectors");
const choiceTargets2 = domArrayArgument(
args,
"choiceTargets",
environment.refs
);
const rawContentTargets = domArrayArgument(
args,
"choiceContentTargets",
environment.refs
);
let slotTargets = domArrayArgument(args, "slotTargets", environment.refs);
if (args.slotTarget != null) {
if (slotTargets.length > 0)
throw new RuleDomainError(
"invalid_type",
"slotTarget and slotTargets cannot be combined"
);
slotTargets = [domArgument$1(args, "slotTarget", environment.refs)];
}
const targets = [];
let contentTargets = [];
let answerTargets = [];
if (["single", "multiple", "judge"].includes(node.type)) {
contentTargets = rawContentTargets.length ? rawContentTargets : choiceTargets2;
if (choiceTargets2.length !== node.options.length || contentTargets.length !== node.options.length || slotTargets.length !== 0)
throw new RuleDomainError(
"binding_target_mismatch",
"choice target count does not match question options"
);
const selected2 = selectedStrategy(args);
const readSelector = optionalString(args, "readSelector");
for (let index = 0; index < node.options.length; index += 1) {
const option = node.options[index];
const clickTarget = choiceTargets2[index];
if (!option || !clickTarget)
throw new RuleDomainError(
"binding_target_mismatch",
"choice target is missing"
);
targets.push(
createDomChoiceBindingTarget({
optionId: option.id,
clickTarget,
readTarget: queryReadTarget(clickTarget, readSelector),
selected: selected2,
location: environment.location
})
);
}
answerTargets = choiceTargets2;
} else {
if (slotTargets.length !== node.slots.length || choiceTargets2.length !== 0 || rawContentTargets.length !== 0)
throw new RuleDomainError(
"binding_target_mismatch",
"slot target count does not match question slots"
);
for (let index = 0; index < node.slots.length; index += 1) {
const slot = node.slots[index];
const element = slotTargets[index];
if (!slot || !element)
throw new RuleDomainError(
"binding_target_mismatch",
"slot target is missing"
);
targets.push(
createDomWriteBindingTarget({
slotId: slot.id,
element,
selectOptions: slot.options
})
);
}
answerTargets = slotTargets;
}
const fingerprintSource = {
stem,
contentTargets,
answerTargets,
stemStripSelectors,
optionStripSelectors
};
const capturedDomFingerprint = domFingerprint(fingerprintSource);
const capturedQuestionFingerprint = questionNodeHash(node);
return {
readCurrentFingerprint: () => {
const current = domFingerprint(fingerprintSource);
return current === capturedDomFingerprint ? capturedQuestionFingerprint : current;
},
targets
};
}
function captureCall(operation) {
try {
return operation();
} catch (error) {
if (error instanceof RuleCaptureRegistryError)
throw new RuleDomainError(error.code, error.message);
throw error;
}
}
function registerLeafDom(args, environment) {
const node = parseLeafNode(args.node);
captureCall(
() => environment.capture.registerLeaf(
node,
leafDomRegistration(node, args, environment)
)
);
return node.path;
}
function registerLeafBindingDom(args, environment) {
const node = parseLeafNode(args.node);
captureCall(
() => environment.capture.registerLeafBinding(
node,
leafDomRegistration(node, args, environment)
)
);
return node.path;
}
function registerTree(args, environment) {
const parsed = QuestionNodeSchema.safeParse(args.root);
if (!parsed.success)
throw new RuleDomainError(
"invalid_question_node",
"capture root is not a valid question tree"
);
try {
assertValidQuestionTree(parsed.data);
} catch (error) {
throw new RuleDomainError(
"invalid_question_node",
error instanceof Error ? error.message : "invalid question tree"
);
}
captureCall(() => environment.capture.registerTree(parsed.data));
return parsed.data.path;
}
function registerCapturePrimitives(registry, environment) {
registry.register({
id: "capture.registerLeafDom",
phases: ["capture"],
capability: "dom-read",
execute: ({ args }) => registerLeafDom(args, environment)
});
registry.register({
id: "capture.registerLeafBindingDom",
phases: ["capture"],
capability: "dom-read",
execute: ({ args }) => registerLeafBindingDom(args, environment)
});
registry.register({
id: "capture.registerTree",
phases: ["capture"],
execute: ({ args }) => registerTree(args, environment)
});
registry.register({
id: "capture.harvestLeaf",
phases: ["capture"],
capability: "dom-read",
execute: ({ args }) => captureCall(
() => environment.capture.registerHarvestLeaf(args.node, args.values)
)
});
registry.register({
id: "capture.finish",
phases: ["capture"],
execute: () => captureCall(() => environment.capture.finish())
});
}
const READ_PHASES = [
"match",
"capture",
"diagnostic",
"lifecycle",
"fill"
];
const SAFE_PROPERTIES = /* @__PURE__ */ new Set([
"value",
"checked",
"selected",
"disabled",
"tagName",
"type",
"name",
"id",
"className"
]);
function stringArgument$1(args, name) {
const value = args[name];
if (typeof value !== "string")
throw new RuleDomainError("invalid_type", `${name} must be a string`);
return value;
}
function domArgument(args, name, refs) {
return refs.getDom(args[name]);
}
function stringArrayArgument(args, name) {
const value = args[name];
if (value == null) return [];
if (!Array.isArray(value) || value.length > 64)
throw new RuleDomainError(
"invalid_type",
`${name} must be an array with at most 64 strings`
);
return value.map((item) => {
if (typeof item !== "string" || !item || item.length > 1024)
throw new RuleDomainError(
"invalid_type",
`${name} must contain non-empty bounded strings`
);
return item;
});
}
function queryRoot(args, refs, fallback) {
const from = args.from;
if (from == null) return fallback;
try {
return refs.getDom(from);
} catch (domError) {
try {
return refs.getFrame(from);
} catch {
throw domError;
}
}
}
function safeQuery(root, selector) {
try {
return root.querySelector(selector);
} catch (error) {
throw new RuleDomainError(
"invalid_selector",
error instanceof Error ? error.message : "invalid selector"
);
}
}
function safeQueryAll(root, selector) {
try {
return Array.from(root.querySelectorAll(selector));
} catch (error) {
throw new RuleDomainError(
"invalid_selector",
error instanceof Error ? error.message : "invalid selector"
);
}
}
function safeXPath(root, xpath, all) {
var _a2;
const document2 = root.nodeType === 9 ? root : root.ownerDocument;
if (!document2)
throw new RuleDomainError("xpath_unavailable", "XPath document is missing");
const XPathResult = (_a2 = document2.defaultView) == null ? void 0 : _a2.XPathResult;
if (!XPathResult)
throw new RuleDomainError("xpath_unavailable", "XPath is unavailable");
try {
if (!all) {
const result = document2.evaluate(
xpath,
root,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
return (result == null ? void 0 : result.nodeType) === 1 ? [result] : [];
}
const iterator = document2.evaluate(
xpath,
root,
null,
XPathResult.ORDERED_NODE_ITERATOR_TYPE,
null
);
const elements = [];
let node = iterator.iterateNext();
while (node) {
if (node.nodeType === 1) elements.push(node);
node = iterator.iterateNext();
}
return elements;
} catch (error) {
throw new RuleDomainError(
"invalid_xpath",
error instanceof Error ? error.message : "invalid XPath"
);
}
}
function intervalArgument(args) {
const value = args.intervalMs ?? 25;
if (!Number.isInteger(value) || value < 10 || value > 1e3)
throw new RuleDomainError(
"invalid_type",
"intervalMs must be an integer between 10 and 1000"
);
return value;
}
function boundedIntegerArgument(args, name, minimum, maximum) {
const value = args[name];
if (!Number.isInteger(value) || value < minimum || value > maximum)
throw new RuleDomainError(
"invalid_type",
`${name} must be an integer between ${minimum} and ${maximum}`
);
return value;
}
function findSameOriginFrameDocument(root, selector, maxDepth, maxFrames) {
const queue = [
{ document: root, depth: 0 }
];
const seen = /* @__PURE__ */ new Set();
let frames = 0;
while (queue.length > 0) {
const current = queue.shift();
if (!current || seen.has(current.document)) continue;
seen.add(current.document);
if (safeQuery(current.document, selector)) return current.document;
if (current.depth >= maxDepth) continue;
for (const element of safeQueryAll(current.document, "iframe")) {
frames += 1;
if (frames > maxFrames) return null;
let frameDocument = null;
try {
frameDocument = element.contentDocument;
} catch {
frameDocument = null;
}
if (frameDocument && !seen.has(frameDocument))
queue.push({ document: frameDocument, depth: current.depth + 1 });
}
}
return null;
}
function collectSameOriginFrameDocuments(root, selector, maxDepth, maxFrames, maxResults) {
const queue = [
{ document: root, depth: 0 }
];
const seen = /* @__PURE__ */ new Set();
const found = [];
let frames = 0;
while (queue.length > 0) {
const current = queue.shift();
if (!current || seen.has(current.document)) continue;
seen.add(current.document);
if (safeQuery(current.document, selector)) {
found.push(current.document);
if (found.length >= maxResults) return found;
}
if (current.depth >= maxDepth) continue;
for (const element of safeQueryAll(current.document, "iframe")) {
frames += 1;
if (frames > maxFrames) return found;
let frameDocument = null;
try {
frameDocument = element.contentDocument;
} catch {
frameDocument = null;
}
if (frameDocument && !seen.has(frameDocument))
queue.push({ document: frameDocument, depth: current.depth + 1 });
}
}
return found;
}
function waitForSameOriginFrames(args, document2, refs, signal) {
const selector = stringArgument$1(args, "selector");
const maxDepth = boundedIntegerArgument(args, "maxDepth", 0, 8);
const maxFrames = boundedIntegerArgument(args, "maxFrames", 1, 128);
const maxResults = boundedIntegerArgument(args, "maxResults", 1, 32);
const waitMs = boundedIntegerArgument(args, "waitMs", 0, 8e3);
const settleMs = boundedIntegerArgument(args, "settleMs", 0, 2e3);
const intervalMs = intervalArgument(args);
const find = () => collectSameOriginFrameDocuments(
document2,
selector,
maxDepth,
maxFrames,
maxResults
);
const wrap = (documents) => documents.map((item) => refs.createFrameRef(item));
if (waitMs === 0) return wrap(find());
return new Promise((resolve, reject) => {
const startedAt = Date.now();
let lastCount = -1;
let changedAt = startedAt;
const cleanup = () => {
clearInterval(timer);
signal.removeEventListener("abort", onAbort);
};
const onAbort = () => {
cleanup();
reject(
new RuleExecutionError(
"cancelled",
"frame.findAllSameOrigin cancelled"
)
);
};
const timer = setInterval(() => {
try {
const found = find();
const now = Date.now();
if (found.length !== lastCount) {
lastCount = found.length;
changedAt = now;
}
if (found.length > 0 && now - changedAt >= settleMs || now - startedAt >= waitMs) {
cleanup();
resolve(wrap(found));
}
} catch (error) {
cleanup();
reject(error);
}
}, intervalMs);
signal.addEventListener("abort", onAbort, { once: true });
});
}
function waitForSameOriginFrame(args, document2, refs, signal) {
const selector = stringArgument$1(args, "selector");
const maxDepth = boundedIntegerArgument(args, "maxDepth", 0, 8);
const maxFrames = boundedIntegerArgument(args, "maxFrames", 1, 128);
const waitMs = boundedIntegerArgument(args, "waitMs", 0, 8e3);
const intervalMs = intervalArgument(args);
const find = () => findSameOriginFrameDocument(document2, selector, maxDepth, maxFrames);
const immediate = find();
if (immediate) return refs.createFrameRef(immediate);
if (waitMs === 0) return null;
return new Promise((resolve, reject) => {
const startedAt = Date.now();
const cleanup = () => {
clearInterval(timer);
signal.removeEventListener("abort", onAbort);
};
const onAbort = () => {
cleanup();
reject(
new RuleExecutionError("cancelled", "frame.findSameOrigin cancelled")
);
};
const timer = setInterval(() => {
try {
const found = find();
if (found) {
cleanup();
resolve(refs.createFrameRef(found));
} else if (Date.now() - startedAt >= waitMs) {
cleanup();
resolve(null);
}
} catch (error) {
cleanup();
reject(error);
}
}, intervalMs);
signal.addEventListener("abort", onAbort, { once: true });
});
}
function assertRevealTarget(element, location2, depth = 0) {
const view = element.ownerDocument.defaultView;
if (!view || !(element instanceof view.HTMLElement))
throw new RuleExecutionError(
"security_violation",
"ui.reveal target must be an HTMLElement"
);
const htmlElement2 = element;
const tagName = htmlElement2.tagName.toLowerCase();
if (tagName === "form")
throw new RuleExecutionError(
"security_violation",
"ui.reveal cannot activate forms"
);
if (htmlElement2 instanceof view.HTMLButtonElement) {
if (htmlElement2.type !== "button")
throw new RuleExecutionError(
"security_violation",
"ui.reveal cannot activate submit controls"
);
}
if (htmlElement2 instanceof view.HTMLInputElement) {
if (["submit", "reset", "image"].includes(htmlElement2.type))
throw new RuleExecutionError(
"security_violation",
"ui.reveal cannot activate submit controls"
);
}
const label = htmlElement2 instanceof view.HTMLInputElement ? htmlElement2.value : "";
if (/提交|交卷|完成考试|\bsubmit\b/iu.test(
`${htmlElement2.textContent ?? ""}
${label}`
))
throw new RuleExecutionError(
"security_violation",
"ui.reveal cannot activate submit controls"
);
if (htmlElement2 instanceof view.HTMLLabelElement && depth === 0) {
const control = htmlElement2.control;
if (control) assertRevealTarget(control, location2, depth + 1);
}
if (htmlElement2 instanceof view.HTMLAnchorElement) {
const target = new URL(htmlElement2.href, location2.href);
if (target.protocol === "javascript:" || target.origin !== location2.origin)
throw new RuleExecutionError(
"security_violation",
"ui.reveal cannot navigate outside the current origin"
);
}
return htmlElement2;
}
function registerDomPrimitives(registry, environment) {
const { document: document2, location: location2, refs } = environment;
registry.register({
id: "dom.queryCss",
phases: READ_PHASES,
capability: "dom-read",
execute: ({ args }) => {
const element = safeQuery(
queryRoot(args, refs, document2),
stringArgument$1(args, "selector")
);
return element ? refs.createDomRef(element) : null;
}
});
registry.register({
id: "dom.queryCssAll",
phases: READ_PHASES,
capability: "dom-read",
execute: ({ args }) => safeQueryAll(
queryRoot(args, refs, document2),
stringArgument$1(args, "selector")
).map((element) => refs.createDomRef(element))
});
registry.register({
id: "dom.queryXPath",
phases: READ_PHASES,
capability: "dom-read",
execute: ({ args }) => {
const element = safeXPath(
queryRoot(args, refs, document2),
stringArgument$1(args, "xpath"),
false
)[0];
return element ? refs.createDomRef(element) : null;
}
});
registry.register({
id: "dom.queryXPathAll",
phases: READ_PHASES,
capability: "dom-read",
execute: ({ args }) => safeXPath(
queryRoot(args, refs, document2),
stringArgument$1(args, "xpath"),
true
).map((element) => refs.createDomRef(element))
});
registry.register({
id: "dom.text",
phases: READ_PHASES,
capability: "dom-read",
execute: ({ args }) => domArgument(args, "target", refs).textContent ?? ""
});
registry.register({
id: "dom.content",
phases: READ_PHASES,
capability: "dom-read",
execute: ({ args, signal }) => {
try {
return serializeDomQuestionContent(domArgument(args, "target", refs), {
stripSelectors: stringArrayArgument(args, "stripSelectors"),
signal
});
} catch (error) {
if (error instanceof DomContentError) {
if (error.code === "budget_exceeded" || error.code === "cancelled")
throw new RuleExecutionError(error.code, error.message);
throw new RuleDomainError(error.code, error.message);
}
throw error;
}
}
});
registry.register({
id: "dom.attr",
phases: READ_PHASES,
capability: "dom-read",
execute: ({ args }) => domArgument(args, "target", refs).getAttribute(
stringArgument$1(args, "name")
)
});
registry.register({
id: "dom.property",
phases: READ_PHASES,
capability: "dom-read",
execute: ({ args }) => {
const name = stringArgument$1(args, "name");
if (!SAFE_PROPERTIES.has(name))
throw new RuleExecutionError(
"security_violation",
`DOM property is not exposed: ${name}`
);
return domArgument(args, "target", refs)[name];
}
});
registry.register({
id: "dom.closest",
phases: READ_PHASES,
capability: "dom-read",
execute: ({ args }) => {
let element;
try {
element = domArgument(args, "target", refs).closest(
stringArgument$1(args, "selector")
);
} catch (error) {
throw new RuleDomainError(
"invalid_selector",
error instanceof Error ? error.message : "invalid selector"
);
}
return element ? refs.createDomRef(element) : null;
}
});
registry.register({
id: "dom.parent",
phases: READ_PHASES,
capability: "dom-read",
execute: ({ args }) => {
const parent = domArgument(args, "target", refs).parentElement;
return parent ? refs.createDomRef(parent) : null;
}
});
registry.register({
id: "dom.children",
phases: READ_PHASES,
capability: "dom-read",
execute: ({ args }) => Array.from(domArgument(args, "target", refs).children).map(
(element) => refs.createDomRef(element)
)
});
registry.register({
id: "dom.index",
phases: READ_PHASES,
capability: "dom-read",
execute: ({ args }) => {
const element = domArgument(args, "target", refs);
return element.parentElement ? Array.from(element.parentElement.children).indexOf(element) : -1;
}
});
registry.register({
id: "frame.list",
phases: READ_PHASES,
capability: "frame-read",
execute: ({ args }) => safeQueryAll(queryRoot(args, refs, document2), "iframe").map(
(frame) => refs.createDomRef(frame)
)
});
registry.register({
id: "frame.enter",
phases: READ_PHASES,
capability: "frame-read",
execute: ({ args }) => {
const frame = domArgument(args, "target", refs);
if (frame.tagName.toLowerCase() !== "iframe")
throw new RuleDomainError("invalid_type", "target is not an iframe");
let frameDocument;
try {
frameDocument = frame.contentDocument;
} catch {
return null;
}
return frameDocument ? refs.createFrameRef(frameDocument) : null;
}
});
registry.register({
id: "frame.findSameOrigin",
phases: READ_PHASES,
capability: "frame-read",
execute: ({ args, signal }) => waitForSameOriginFrame(args, document2, refs, signal)
});
registry.register({
id: "frame.findAllSameOrigin",
phases: READ_PHASES,
capability: "frame-read",
execute: ({ args, signal }) => waitForSameOriginFrames(args, document2, refs, signal)
});
registry.register({
id: "page.location",
phases: READ_PHASES,
capability: "runtime-read",
execute: () => ({
href: location2.href,
origin: location2.origin,
protocol: location2.protocol,
host: location2.host,
pathname: location2.pathname
})
});
registry.register({
id: "page.queryParam",
phases: READ_PHASES,
capability: "runtime-read",
execute: ({ args }) => new URL(location2.href).searchParams.get(stringArgument$1(args, "name"))
});
registry.register({
id: "ui.reveal",
phases: ["lifecycle"],
capability: "ui-reveal",
execute: ({ args }) => {
const element = assertRevealTarget(
domArgument(args, "target", refs),
location2
);
element.click();
return true;
}
});
registry.register({
id: "wait.selector",
phases: READ_PHASES,
capability: "dom-read",
execute: ({ args, signal }) => {
const root = queryRoot(args, refs, document2);
const selector = stringArgument$1(args, "selector");
const intervalMs = intervalArgument(args);
const immediate = safeQuery(root, selector);
if (immediate) return refs.createDomRef(immediate);
return new Promise((resolve, reject) => {
const cleanup = () => {
clearInterval(timer);
signal.removeEventListener("abort", onAbort);
};
const onAbort = () => {
cleanup();
reject(new RuleExecutionError("cancelled", "wait.selector cancelled"));
};
const timer = setInterval(() => {
try {
const element = safeQuery(root, selector);
if (!element) return;
cleanup();
resolve(refs.createDomRef(element));
} catch (error) {
cleanup();
reject(error);
}
}, intervalMs);
signal.addEventListener("abort", onAbort, { once: true });
});
}
});
}
function maxTriggers(args) {
const value = args.maxTriggers;
if (!Number.isInteger(value) || value <= 0 || value > 1e3)
throw new RuleDomainError(
"invalid_type",
"maxTriggers must be an integer between 1 and 1000"
);
return value;
}
function registerObserverPrimitives(registry, environment) {
const { window: window2, document: document2, MutationObserver, refs, resources, emit } = environment;
registry.register({
id: "observe.mutation",
phases: ["lifecycle"],
capability: "dom-read",
execute: ({ args }) => {
const limit = maxTriggers(args);
const target = args.target == null ? document2.documentElement : refs.getDom(args.target);
if (!target)
throw new RuleDomainError(
"missing_target",
"mutation target is missing"
);
let active2 = true;
let trigger = 0;
const observer = new MutationObserver((mutations) => {
if (!active2) return;
trigger += 1;
emit({
event: "dom-change",
payload: { mutationCount: mutations.length, trigger }
});
if (trigger >= limit) cleanup();
});
const cleanup = () => {
if (!active2) return;
active2 = false;
observer.disconnect();
};
resources.add(cleanup);
observer.observe(target, {
attributes: true,
childList: true,
subtree: true
});
return true;
}
});
let urlHookInstalled = false;
registry.register({
id: "observe.urlChange",
phases: ["lifecycle"],
capability: "runtime-read",
execute: ({ args }) => {
if (urlHookInstalled)
throw new RuleExecutionError(
"security_violation",
"URL observer is already installed"
);
urlHookInstalled = true;
const limit = maxTriggers(args);
const history = window2.history;
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
let active2 = true;
let trigger = 0;
const cleanup = () => {
if (!active2) return;
active2 = false;
window2.removeEventListener("popstate", notify);
if (history.pushState === wrappedPushState)
history.pushState = originalPushState;
if (history.replaceState === wrappedReplaceState)
history.replaceState = originalReplaceState;
urlHookInstalled = false;
};
const notify = () => {
if (!active2) return;
trigger += 1;
emit({
event: "url-change",
payload: {
origin: window2.location.origin,
pathname: window2.location.pathname
}
});
if (trigger >= limit) cleanup();
};
const wrappedPushState = function pushState(data, unused, url) {
originalPushState.call(history, data, unused, url);
notify();
};
const wrappedReplaceState = function replaceState(data, unused, url) {
originalReplaceState.call(history, data, unused, url);
notify();
};
history.pushState = wrappedPushState;
history.replaceState = wrappedReplaceState;
window2.addEventListener("popstate", notify);
resources.add(cleanup);
return true;
}
});
}
const TRANSFORM_PHASES = [
"match",
"capture",
"diagnostic",
"lifecycle"
];
const MAX_TRANSFORM_TEXT_BYTES = 128 * 1024;
const BLOCKED_HTML_CONTENT = /<(script|style|noscript)\b[^>]*>[\s\S]*?<\/\1\s*>/giu;
function stringArgument(args, name) {
const value = args[name];
if (typeof value !== "string")
throw new RuleDomainError("invalid_type", `${name} must be a string`);
if (new TextEncoder().encode(value).length > MAX_TRANSFORM_TEXT_BYTES)
throw new RuleExecutionError(
"budget_exceeded",
`${name} exceeds transform byte limit`
);
return value;
}
function sanitizeQuestionContent(value) {
const withoutBlockedContent = value.replace(BLOCKED_HTML_CONTENT, "");
return collapseWs(
parseQuestionContent(withoutBlockedContent).map(
(part) => part.type === "image" ? serializeImageToken(part.value) : serializeQuestionText(part.value)
).join("")
);
}
function registerTransformPrimitives(registry) {
registry.register({
id: "content.sanitize",
phases: TRANSFORM_PHASES,
execute: ({ args, signal }) => {
if (signal.aborted)
throw new RuleExecutionError(
"cancelled",
"content.sanitize was cancelled"
);
return sanitizeQuestionContent(stringArgument(args, "value"));
}
});
registry.register({
id: "text.stripOptionPrefix",
phases: TRANSFORM_PHASES,
execute: ({ args }) => stripOptionPrefix(stringArgument(args, "value"))
});
registry.register({
id: "text.includes",
phases: TRANSFORM_PHASES,
execute: ({ args }) => stringArgument(args, "value").includes(stringArgument(args, "search"))
});
registry.register({
id: "text.normalizeTruth",
phases: TRANSFORM_PHASES,
execute: ({ args }) => normalizeTruth(stringArgument(args, "value"))
});
registry.register({
id: "question.normalizeLeafType",
phases: TRANSFORM_PHASES,
execute: ({ args }) => normalizeLeafQuestionType(stringArgument(args, "value"))
});
registry.register({
id: "array.append",
phases: TRANSFORM_PHASES,
execute: ({ args }) => {
const items = JsonRuleValueSchema.safeParse(args.items);
const value = JsonRuleValueSchema.safeParse(args.value);
if (!items.success || !Array.isArray(items.data) || !value.success)
throw new RuleDomainError(
"invalid_type",
"array.append requires JSON items and value"
);
const maxItems = args.maxItems;
if (!Number.isInteger(maxItems) || maxItems <= 0 || maxItems > RULE_HARD_LIMITS.maxLoopIterations)
throw new RuleDomainError(
"invalid_type",
"maxItems must be a bounded positive integer"
);
if (items.data.length >= maxItems)
throw new RuleExecutionError(
"budget_exceeded",
"array.append item budget exceeded"
);
return [...items.data, value.data];
}
});
}
const RULE_DISPATCHED_EVENTS = /* @__PURE__ */ new Set(["dom-change", "url-change"]);
function registerCoreRulePrimitives(registry, environment) {
const { document: document2, location: location2, refs, capture: capture2, writer, resources, emit } = environment;
registerDomPrimitives(registry, { document: document2, location: location2, refs });
registerTransformPrimitives(registry);
registerCapturePrimitives(registry, { refs, capture: capture2, location: location2 });
registerAnswerWritePrimitives(registry, writer);
const view = document2.defaultView;
if (!view) return;
registerObserverPrimitives(registry, {
window: view,
document: document2,
MutationObserver: view.MutationObserver,
refs,
resources,
emit: emit ?? (() => void 0)
});
}
const FORBIDDEN_KEYS$2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
class ReturnSignal {
constructor(value) {
this.value = value;
}
}
function validatePositiveLimit(name, value, maximum) {
if (!Number.isInteger(value) || value <= 0 || value > maximum) {
throw new RuleExecutionError(
"security_violation",
`invalid runtime limit ${name}: ${value}`
);
}
}
function resolveLimits(policy, requested) {
validatePositiveLimit(
"maxSteps",
policy.limits.maxSteps,
RULE_HARD_LIMITS.maxSteps
);
validatePositiveLimit(
"maxWallMs",
policy.limits.maxWallMs,
RULE_HARD_LIMITS.maxWallMs
);
validatePositiveLimit(
"maxAsyncMs",
policy.limits.maxAsyncMs,
RULE_HARD_LIMITS.maxAsyncMs
);
validatePositiveLimit(
"maxLoopIterations",
policy.limits.maxLoopIterations,
RULE_HARD_LIMITS.maxLoopIterations
);
validatePositiveLimit(
"maxCallDepth",
policy.limits.maxCallDepth,
RULE_HARD_LIMITS.maxCallDepth
);
validatePositiveLimit(
"maxDomRefs",
policy.limits.maxDomRefs,
RULE_HARD_LIMITS.maxDomRefs
);
const limits = { ...policy.limits, ...requested };
for (const key of Object.keys(limits)) {
if (limits[key] > policy.limits[key]) {
throw new RuleExecutionError(
"security_violation",
`rule cannot expand runtime limit: ${key}`
);
}
}
return limits;
}
function checkExecution(state) {
var _a2;
if ((_a2 = state.signal) == null ? void 0 : _a2.aborted) {
throw new RuleExecutionError("cancelled", "rule execution cancelled");
}
if (state.now() - state.startedAt >= state.limits.maxWallMs) {
throw new RuleExecutionError("timeout", "rule execution timed out");
}
}
function consumeStep(state) {
checkExecution(state);
state.steps += 1;
if (state.steps > state.limits.maxSteps) {
throw new RuleExecutionError("budget_exceeded", "rule step budget exceeded");
}
}
function requireBoolean(value, label) {
if (typeof value !== "boolean") {
throw new RuleDomainError("invalid_type", `${label} must be boolean`);
}
return value;
}
function requireString(value, label) {
if (typeof value !== "string") {
throw new RuleDomainError("invalid_type", `${label} must be string`);
}
return value;
}
function ownDataProperty$1(value, key) {
const normalizedKey = String(key);
if (FORBIDDEN_KEYS$2.has(normalizedKey)) {
throw new RuleExecutionError(
"security_violation",
`forbidden property path: ${normalizedKey}`
);
}
if (typeof value !== "object" && typeof value !== "function" || value == null) {
throw new RuleDomainError("invalid_type", "path source must be an object");
}
const descriptor = Object.getOwnPropertyDescriptor(value, normalizedKey);
if (!descriptor) return null;
if (!("value" in descriptor)) {
throw new RuleExecutionError(
"security_violation",
`property getter is not readable: ${normalizedKey}`
);
}
return descriptor.value;
}
function isJsonComparable(value) {
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
return typeof value !== "number" || Number.isFinite(value);
}
if (Array.isArray(value)) return value.every(isJsonComparable);
if (typeof value !== "object") return false;
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null) return false;
const record = value;
return Object.keys(record).every((key) => {
if (FORBIDDEN_KEYS$2.has(key)) return false;
const descriptor = Object.getOwnPropertyDescriptor(record, key);
return Boolean(
descriptor && "value" in descriptor && isJsonComparable(descriptor.value)
);
});
}
function valuesEqual(left, right) {
if (Object.is(left, right)) return true;
if (!isJsonComparable(left) || !isJsonComparable(right)) return false;
return canonicalize(left) === canonicalize(right);
}
function compareValues(kind, left, right) {
if (kind === "eq") return valuesEqual(left, right);
if (kind === "ne") return !valuesEqual(left, right);
if (typeof left !== "number" && typeof left !== "string" || typeof left !== typeof right) {
throw new RuleDomainError(
"invalid_type",
`comparison ${kind} requires matching strings or numbers`
);
}
if (typeof left === "number") {
const numericRight = right;
if (kind === "gt") return left > numericRight;
if (kind === "gte") return left >= numericRight;
if (kind === "lt") return left < numericRight;
return left <= numericRight;
}
const stringRight = right;
if (kind === "gt") return left > stringRight;
if (kind === "gte") return left >= stringRight;
if (kind === "lt") return left < stringRight;
return left <= stringRight;
}
function formatValue(value) {
if (value === null) return "";
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
return String(value);
}
if (isJsonComparable(value)) return canonicalize(value);
throw new RuleDomainError("invalid_type", "format argument is not JSON data");
}
function restoreVariable(variables, name, previous) {
if (previous.present) variables.set(name, previous.value);
else variables.delete(name);
}
function requireArray(value, label) {
if (!Array.isArray(value)) {
throw new RuleDomainError("invalid_type", `${label} must be an array`);
}
return value;
}
function requireJsonValue(value, label) {
if (!isJsonComparable(value)) {
throw new RuleDomainError("invalid_type", `${label} must be JSON data`);
}
return value;
}
function assertCollectionBudget(length, maxIterations, state) {
if (!Number.isInteger(maxIterations) || maxIterations <= 0 || maxIterations > state.limits.maxLoopIterations || length > maxIterations) {
throw new RuleExecutionError(
"budget_exceeded",
"collection iteration budget exceeded"
);
}
}
function assertDistinctVariables(names) {
const present = names.filter((name) => name != null);
if (new Set(present).size !== present.length || present.some((name) => name.startsWith("$") || FORBIDDEN_KEYS$2.has(name))) {
throw new RuleExecutionError(
"security_violation",
"collection variables must be distinct non-reserved names"
);
}
}
async function raceBounded(operation, state, timeoutMs) {
checkExecution(state);
const remainingWall = Math.max(
1,
state.limits.maxWallMs - (state.now() - state.startedAt)
);
const boundedTimeout = Math.min(
timeoutMs,
state.limits.maxAsyncMs,
remainingWall
);
const controller = new AbortController();
let timer;
let onAbort;
const boundary = new Promise((_, reject) => {
timer = setTimeout(() => {
reject(
new RuleExecutionError("timeout", "rule async operation timed out")
);
controller.abort();
}, boundedTimeout);
if (state.signal) {
onAbort = () => {
reject(new RuleExecutionError("cancelled", "rule execution cancelled"));
controller.abort();
};
state.signal.addEventListener("abort", onAbort, { once: true });
}
});
try {
return await Promise.race([operation(controller.signal), boundary]);
} finally {
if (timer !== void 0) clearTimeout(timer);
if (state.signal && onAbort)
state.signal.removeEventListener("abort", onAbort);
controller.abort();
}
}
async function evaluateExpr(expr, variables, state) {
consumeStep(state);
switch (expr.op) {
case "literal":
return expr.value;
case "var":
if (!variables.has(expr.name)) {
throw new RuleDomainError(
"missing_variable",
`missing variable: ${expr.name}`
);
}
return variables.get(expr.name);
case "path": {
let value = await evaluateExpr(expr.from, variables, state);
for (const segment of expr.path) value = ownDataProperty$1(value, segment);
return value;
}
case "coalesce":
for (const candidate of expr.values) {
const value = await evaluateExpr(candidate, variables, state);
if (value !== null && value !== void 0) return value;
}
return null;
case "compare":
return compareValues(
expr.kind,
await evaluateExpr(expr.left, variables, state),
await evaluateExpr(expr.right, variables, state)
);
case "logic": {
if (expr.kind === "and") {
for (const valueExpr of expr.values) {
if (!requireBoolean(
await evaluateExpr(valueExpr, variables, state),
"logic operand"
))
return false;
}
return true;
}
for (const valueExpr of expr.values) {
if (requireBoolean(
await evaluateExpr(valueExpr, variables, state),
"logic operand"
))
return true;
}
return false;
}
case "not":
return !requireBoolean(
await evaluateExpr(expr.value, variables, state),
"not operand"
);
case "array": {
const result = [];
for (const item of expr.items) {
result.push(
requireJsonValue(
await evaluateExpr(item, variables, state),
"array item"
)
);
}
return result;
}
case "object": {
const result = /* @__PURE__ */ Object.create(null);
for (const [key, value] of Object.entries(expr.entries)) {
if (FORBIDDEN_KEYS$2.has(key)) {
throw new RuleExecutionError(
"security_violation",
`forbidden object key: ${key}`
);
}
result[key] = requireJsonValue(
await evaluateExpr(value, variables, state),
`object entry ${key}`
);
}
return result;
}
case "map":
case "filter": {
const items = requireArray(
await evaluateExpr(expr.items, variables, state),
`${expr.op} items`
);
assertCollectionBudget(items.length, expr.maxIterations, state);
assertDistinctVariables([expr.item, expr.index]);
const itemPrevious = {
present: variables.has(expr.item),
value: variables.get(expr.item)
};
const indexPrevious = expr.index ? {
present: variables.has(expr.index),
value: variables.get(expr.index)
} : null;
const result = [];
try {
for (let index = 0; index < items.length; index += 1) {
consumeStep(state);
variables.set(expr.item, items[index]);
if (expr.index) variables.set(expr.index, index);
if (expr.op === "map") {
result.push(
requireJsonValue(
await evaluateExpr(expr.value, variables, state),
"map result"
)
);
} else if (requireBoolean(
await evaluateExpr(expr.when, variables, state),
"filter condition"
)) {
result.push(requireJsonValue(items[index], "filter result"));
}
}
} finally {
restoreVariable(variables, expr.item, itemPrevious);
if (expr.index && indexPrevious)
restoreVariable(variables, expr.index, indexPrevious);
}
return result;
}
case "reduce": {
const items = requireArray(
await evaluateExpr(expr.items, variables, state),
"reduce items"
);
assertCollectionBudget(items.length, expr.maxIterations, state);
assertDistinctVariables([expr.item, expr.index, expr.accumulator]);
let accumulator = requireJsonValue(
await evaluateExpr(expr.initial, variables, state),
"reduce initial value"
);
const itemPrevious = {
present: variables.has(expr.item),
value: variables.get(expr.item)
};
const indexPrevious = expr.index ? {
present: variables.has(expr.index),
value: variables.get(expr.index)
} : null;
const accumulatorPrevious = {
present: variables.has(expr.accumulator),
value: variables.get(expr.accumulator)
};
try {
for (let index = 0; index < items.length; index += 1) {
consumeStep(state);
variables.set(expr.item, items[index]);
if (expr.index) variables.set(expr.index, index);
variables.set(expr.accumulator, accumulator);
accumulator = requireJsonValue(
await evaluateExpr(expr.value, variables, state),
"reduce result"
);
}
} finally {
restoreVariable(variables, expr.item, itemPrevious);
if (expr.index && indexPrevious)
restoreVariable(variables, expr.index, indexPrevious);
restoreVariable(variables, expr.accumulator, accumulatorPrevious);
}
return accumulator;
}
case "string": {
const value = requireString(
await evaluateExpr(expr.value, variables, state),
"string operand"
);
if (expr.kind === "trim") return value.trim();
if (expr.kind === "collapseWs") return value.replace(/\s+/gu, " ").trim();
if (expr.kind === "lower") return value.toLocaleLowerCase();
return value.toLocaleUpperCase();
}
case "regex": {
if (!state.services.regex) {
throw new RuleExecutionError(
"unsupported_expression",
"regex executor is not installed"
);
}
const value = requireString(
await evaluateExpr(expr.value, variables, state),
"regex operand"
);
return raceBounded(
(signal) => {
var _a2, _b;
return (_b = (_a2 = state.services).regex) == null ? void 0 : _b.call(_a2, {
kind: expr.kind,
value,
pattern: expr.pattern,
flags: expr.flags,
replacement: expr.replacement,
signal,
timeoutMs: state.limits.maxAsyncMs
});
},
state,
state.limits.maxAsyncMs
);
}
case "jsonPath": {
if (!state.services.jsonPath) {
throw new RuleExecutionError(
"unsupported_expression",
"JSONPath executor is not installed"
);
}
const value = await evaluateExpr(expr.from, variables, state);
return raceBounded(
async (signal) => {
var _a2, _b;
return (_b = (_a2 = state.services).jsonPath) == null ? void 0 : _b.call(_a2, { value, query: expr.query, signal });
},
state,
state.limits.maxAsyncMs
);
}
case "format": {
const values = /* @__PURE__ */ new Map();
for (const [name, argument] of Object.entries(expr.args)) {
values.set(
name,
formatValue(await evaluateExpr(argument, variables, state))
);
}
return expr.template.replace(
/\{([A-Za-z_][A-Za-z0-9_]*)\}/gu,
(_, name) => values.has(name) ? values.get(name) ?? "" : `{${name}}`
);
}
}
}
async function executeFlow(flow, variables, state, depth) {
if (depth > state.limits.maxCallDepth) {
throw new RuleExecutionError(
"call_depth_exceeded",
"rule call depth exceeded"
);
}
try {
await executeSteps(flow.steps, variables, state, depth);
return null;
} catch (error) {
if (error instanceof ReturnSignal) return error.value;
throw error;
}
}
async function executeSteps(steps, variables, state, depth) {
for (const step of steps) {
consumeStep(state);
switch (step.type) {
case "set":
if (step.name.startsWith("$") || FORBIDDEN_KEYS$2.has(step.name)) {
throw new RuleExecutionError(
"security_violation",
`cannot set reserved variable: ${step.name}`
);
}
variables.set(
step.name,
await evaluateExpr(step.value, variables, state)
);
break;
case "if":
if (requireBoolean(
await evaluateExpr(step.when, variables, state),
"if condition"
))
await executeSteps(step.then, variables, state, depth);
else if (step.else)
await executeSteps(step.else, variables, state, depth);
break;
case "switch": {
const value = await evaluateExpr(step.value, variables, state);
const selected2 = step.cases.find(
(candidate) => valuesEqual(value, candidate.equals)
);
if (selected2)
await executeSteps(selected2.steps, variables, state, depth);
else if (step.default)
await executeSteps(step.default, variables, state, depth);
break;
}
case "forEach": {
const items = await evaluateExpr(step.items, variables, state);
if (!Array.isArray(items)) {
throw new RuleDomainError(
"invalid_type",
"forEach items must be an array"
);
}
if (step.maxIterations > state.limits.maxLoopIterations || items.length > step.maxIterations) {
throw new RuleExecutionError(
"budget_exceeded",
"forEach iteration budget exceeded"
);
}
const itemPrevious = {
present: variables.has(step.item),
value: variables.get(step.item)
};
const indexPrevious = step.index ? {
present: variables.has(step.index),
value: variables.get(step.index)
} : null;
try {
for (let index = 0; index < items.length; index += 1) {
consumeStep(state);
variables.set(step.item, items[index]);
if (step.index) variables.set(step.index, index);
await executeSteps(step.steps, variables, state, depth);
}
} finally {
restoreVariable(variables, step.item, itemPrevious);
if (step.index && indexPrevious)
restoreVariable(variables, step.index, indexPrevious);
}
break;
}
case "while": {
if (step.maxIterations > state.limits.maxLoopIterations) {
throw new RuleExecutionError(
"budget_exceeded",
"while iteration budget exceeded"
);
}
let iterations = 0;
while (requireBoolean(
await evaluateExpr(step.when, variables, state),
"while condition"
)) {
if (iterations >= step.maxIterations) {
throw new RuleExecutionError(
"budget_exceeded",
"while iteration budget exceeded"
);
}
iterations += 1;
consumeStep(state);
await executeSteps(step.steps, variables, state, depth);
}
break;
}
case "callFlow": {
const target = state.flows.get(step.flowId);
if (!target) {
throw new RuleDomainError(
"missing_flow",
`missing flow: ${step.flowId}`
);
}
const childVariables = new Map(variables);
const evaluatedArgs = /* @__PURE__ */ new Map();
for (const [name, argument] of Object.entries(step.args ?? {})) {
evaluatedArgs.set(
name,
await evaluateExpr(argument, variables, state)
);
}
for (const param of target.params ?? []) {
if (evaluatedArgs.has(param))
childVariables.set(param, evaluatedArgs.get(param));
else if (!childVariables.has(param))
throw new RuleDomainError(
"missing_variable",
`missing flow argument: ${param}`
);
}
const result = await executeFlow(
target,
childVariables,
state,
depth + 1
);
if (step.result) variables.set(step.result, result);
break;
}
case "return":
throw new ReturnSignal(
step.value ? await evaluateExpr(step.value, variables, state) : null
);
case "try": {
try {
await executeSteps(step.steps, variables, state, depth);
} catch (error) {
if (!(error instanceof RuleDomainError) || !step.catch) throw error;
const previous = {
present: variables.has("$error"),
value: variables.get("$error")
};
variables.set("$error", error.toRuleValue());
try {
await executeSteps(step.catch, variables, state, depth);
} finally {
restoreVariable(variables, "$error", previous);
}
} finally {
if (step.finally)
await executeSteps(step.finally, variables, state, depth);
}
break;
}
case "primitive": {
const args = /* @__PURE__ */ Object.create(null);
for (const [name, argument] of Object.entries(step.args ?? {})) {
args[name] = await evaluateExpr(argument, variables, state);
}
const invocation = {
id: step.id,
args,
phase: state.phase,
signal: new AbortController().signal,
variables,
requestedCapabilities: state.requestedCapabilities
};
const definition = assertPrimitiveAllowed(
state.registry,
state.policy,
invocation
);
const result = await raceBounded(
(signal) => invokePrimitive(definition, {
...invocation,
signal
}),
state,
step.timeoutMs ?? state.limits.maxAsyncMs
);
if (step.result) variables.set(step.result, result);
break;
}
}
}
}
class RuleInterpreter {
constructor(options) {
__publicField(this, "now");
this.options = options;
this.now = options.now ?? Date.now;
}
async run(flow, options) {
var _a2, _b;
const startedAt = this.now();
(_a2 = options.resources) == null ? void 0 : _a2.bind(options.signal);
try {
const requestedCapabilities = options.capabilities ?? this.options.policy.capabilities;
for (const capability of requestedCapabilities) {
if (!this.options.policy.capabilities.has(capability)) {
throw new RuleExecutionError(
"capability_denied",
`rule cannot expand runtime capability: ${capability}`
);
}
}
const flows = /* @__PURE__ */ new Map();
for (const candidate of [flow, ...options.flows ?? []]) {
const existing = flows.get(candidate.id);
if (existing && existing !== candidate) {
throw new RuleExecutionError(
"security_violation",
`duplicate flow id: ${candidate.id}`
);
}
flows.set(candidate.id, candidate);
}
const variables = /* @__PURE__ */ new Map();
for (const [name, value2] of Object.entries(options.variables ?? {})) {
if (name.startsWith("$") || FORBIDDEN_KEYS$2.has(name)) {
throw new RuleExecutionError(
"security_violation",
`invalid input variable: ${name}`
);
}
variables.set(name, value2);
}
for (const [name, value2] of Object.entries(options.reserved ?? {})) {
if (!name.startsWith("$") || FORBIDDEN_KEYS$2.has(name.slice(1))) {
throw new RuleExecutionError(
"security_violation",
`invalid reserved variable: ${name}`
);
}
variables.set(name, value2);
}
const state = {
phase: options.phase,
policy: this.options.policy,
registry: this.options.registry,
services: this.options.services ?? {},
requestedCapabilities,
signal: options.signal,
limits: resolveLimits(this.options.policy, options.limits),
startedAt,
now: this.now,
flows,
steps: 0
};
checkExecution(state);
const value = await executeFlow(flow, variables, state, 1);
return {
value,
steps: state.steps,
elapsedMs: this.now() - startedAt,
variables: new Map(variables)
};
} catch (error) {
await ((_b = options.resources) == null ? void 0 : _b.dispose());
throw error;
}
}
}
const utf8Length$2 = (value) => new TextEncoder().encode(value).length;
function validateRequest(input) {
if (!input || typeof input !== "object")
throw new Error("invalid regex request");
const request = input;
if (!["test", "extract", "replace"].includes(String(request.kind)))
throw new Error("invalid regex kind");
if (typeof request.value !== "string" || typeof request.pattern !== "string" || request.flags != null && typeof request.flags !== "string" || request.replacement != null && typeof request.replacement !== "string")
throw new Error("invalid regex request");
if (utf8Length$2(request.pattern) > RULE_HARD_LIMITS.maxRegexPatternBytes || utf8Length$2(request.value) > RULE_HARD_LIMITS.maxRegexValueBytes || typeof request.replacement === "string" && utf8Length$2(request.replacement) > RULE_HARD_LIMITS.maxRegexValueBytes)
throw new Error("regex input exceeds byte limit");
const flags = request.flags ?? "";
if (!/^[dgimsuvy]*$/u.test(flags) || new Set(flags).size !== flags.length)
throw new Error("invalid regex flags");
return {
kind: request.kind,
value: request.value,
pattern: request.pattern,
flags,
replacement: request.replacement
};
}
function outputWithinLimit(value) {
try {
return isJsonRuleValue(value) && utf8Length$2(JSON.stringify(value)) <= RULE_HARD_LIMITS.maxRegexValueBytes;
} catch {
return false;
}
}
function isJsonRuleValue(value) {
if (value === null || typeof value === "boolean" || typeof value === "string")
return true;
if (typeof value === "number") return Number.isFinite(value);
if (Array.isArray(value)) return value.every(isJsonRuleValue);
if (!value || typeof value !== "object") return false;
return Object.keys(value).every(
(key) => !["__proto__", "prototype", "constructor"].includes(key) && isJsonRuleValue(value[key])
);
}
function parseWorkerResponse(input) {
if (!input || typeof input !== "object")
throw new RuleExecutionError(
"security_violation",
"invalid regex worker response"
);
const response = input;
if (response.ok === true && Object.hasOwn(response, "value")) {
const value = response.value;
if (!outputWithinLimit(value))
throw new RuleExecutionError(
"security_violation",
"regex worker output exceeds byte limit"
);
return { ok: true, value };
}
if (response.ok === false && (response.code === "regex_error" || response.code === "security_violation") && typeof response.error === "string") {
return {
ok: false,
code: response.code,
error: response.error
};
}
throw new RuleExecutionError(
"security_violation",
"invalid regex worker response"
);
}
function validateExecutionRequest(request) {
if (!Number.isInteger(request.timeoutMs) || request.timeoutMs <= 0 || request.timeoutMs > RULE_HARD_LIMITS.maxAsyncMs)
throw new RuleExecutionError("security_violation", "invalid regex timeout");
const validated = validateRequest(request);
if (validated.kind !== request.kind)
throw new RuleExecutionError("security_violation", "invalid regex request");
}
class IsolatedRegexExecutor {
constructor(createWorker) {
this.createWorker = createWorker;
}
execute(request) {
try {
validateExecutionRequest(request);
} catch (error) {
return Promise.reject(
error instanceof RuleExecutionError ? error : new RuleExecutionError(
"security_violation",
error instanceof Error ? error.message : "invalid regex request"
)
);
}
if (request.signal.aborted) {
return Promise.reject(
new RuleExecutionError("cancelled", "regex execution cancelled")
);
}
let worker;
try {
worker = this.createWorker();
} catch (error) {
return Promise.reject(
new RuleDomainError(
"regex_worker_failed",
error instanceof Error ? error.message : "regex worker failed"
)
);
}
return new Promise((resolve, reject) => {
let settled = false;
const finish = (callback) => {
if (settled) return;
settled = true;
clearTimeout(timer);
request.signal.removeEventListener("abort", onAbort);
worker.removeEventListener("message", onMessage);
worker.removeEventListener("error", onError);
worker.terminate();
callback();
};
const onMessage = (event) => {
try {
const response = parseWorkerResponse(
event == null ? void 0 : event.data
);
if (response.ok) finish(() => resolve(response.value));
else if (response.code === "security_violation")
finish(
() => reject(
new RuleExecutionError("security_violation", response.error)
)
);
else
finish(
() => reject(new RuleDomainError("regex_error", response.error))
);
} catch (error) {
finish(() => reject(error));
}
};
const onError = (event) => finish(
() => reject(
new RuleDomainError(
"regex_worker_failed",
(event == null ? void 0 : event.message) ?? "regex worker failed"
)
)
);
const onAbort = () => finish(
() => reject(
new RuleExecutionError("cancelled", "regex execution cancelled")
)
);
const timer = setTimeout(
() => finish(
() => reject(
new RuleExecutionError("timeout", "regex execution timed out")
)
),
request.timeoutMs
);
request.signal.addEventListener("abort", onAbort, { once: true });
worker.addEventListener("message", onMessage);
worker.addEventListener("error", onError);
try {
worker.postMessage({
kind: request.kind,
value: request.value,
pattern: request.pattern,
flags: request.flags,
replacement: request.replacement
});
} catch (error) {
finish(
() => reject(
new RuleDomainError(
"regex_worker_failed",
error instanceof Error ? error.message : "regex worker failed"
)
)
);
}
});
}
}
const FORBIDDEN_KEYS$1 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
function readIdentifier(query, start) {
const match = /^[A-Za-z_][A-Za-z0-9_-]*/u.exec(query.slice(start));
if (!match) throw new Error(`invalid JSONPath at ${start}`);
if (FORBIDDEN_KEYS$1.has(match[0])) throw new Error("forbidden JSONPath key");
return [match[0], start + match[0].length];
}
function readQuotedKey(query, start) {
const quote = query[start];
if (quote !== "'" && quote !== '"')
throw new Error(`invalid JSONPath at ${start}`);
let key = "";
let index = start + 1;
while (index < query.length) {
const character = query[index];
if (character === quote) {
if (FORBIDDEN_KEYS$1.has(key)) throw new Error("forbidden JSONPath key");
return [key, index + 1];
}
if (character === "\\") {
index += 1;
const escaped = query[index];
if (escaped !== quote && escaped !== "\\")
throw new Error(`invalid JSONPath escape at ${index}`);
key += escaped;
index += 1;
continue;
}
if (!character || character.charCodeAt(0) < 32)
throw new Error(`invalid JSONPath key at ${index}`);
key += character;
index += 1;
}
throw new Error("unterminated JSONPath key");
}
function parseJsonPath(query) {
if (query.length === 0 || query.length > 4096 || query[0] !== "$")
throw new Error("invalid JSONPath root");
const tokens = [];
let index = 1;
while (index < query.length) {
if (query.startsWith("..", index)) {
const [key, next] = readIdentifier(query, index + 2);
tokens.push({ type: "recursive-property", key });
index = next;
continue;
}
if (query[index] === ".") {
index += 1;
if (query[index] === "*") {
tokens.push({ type: "wildcard" });
index += 1;
continue;
}
const [key, next] = readIdentifier(query, index);
tokens.push({ type: "property", key });
index = next;
continue;
}
if (query[index] === "[") {
index += 1;
if (query[index] === "*") {
if (query[index + 1] !== "]")
throw new Error(`invalid JSONPath wildcard at ${index}`);
tokens.push({ type: "wildcard" });
index += 2;
continue;
}
if (query[index] === "'" || query[index] === '"') {
const [key, next2] = readQuotedKey(query, index);
if (query[next2] !== "]")
throw new Error(`invalid JSONPath bracket at ${next2}`);
tokens.push({ type: "property", key });
index = next2 + 1;
continue;
}
const number = /^\d+/u.exec(query.slice(index));
if (!number) throw new Error(`invalid JSONPath bracket at ${index}`);
const next = index + number[0].length;
if (query[next] !== "]")
throw new Error(`invalid JSONPath index at ${index}`);
tokens.push({ type: "index", index: Number(number[0]) });
index = next + 1;
continue;
}
throw new Error(`invalid JSONPath at ${index}`);
}
return tokens;
}
function assertActive$1(signal) {
if (signal == null ? void 0 : signal.aborted)
throw new RuleExecutionError("cancelled", "JSONPath execution cancelled");
}
function ownDataProperty(value, key) {
if (FORBIDDEN_KEYS$1.has(key))
throw new RuleExecutionError(
"security_violation",
`forbidden JSONPath key: ${key}`
);
if (!value || typeof value !== "object") return void 0;
const descriptor = Object.getOwnPropertyDescriptor(value, key);
if (!descriptor) return void 0;
if (!("value" in descriptor))
throw new RuleExecutionError(
"security_violation",
`JSONPath getter is not readable: ${key}`
);
return descriptor.value;
}
function enumerableChildren(value) {
if (!value || typeof value !== "object") return [];
const children = [];
for (const key of Object.keys(value)) {
if (FORBIDDEN_KEYS$1.has(key)) continue;
const child = ownDataProperty(value, key);
if (child !== void 0) children.push(child);
}
return children;
}
function executeJsonPath(value, query, options = {}) {
assertActive$1(options.signal);
const tokens = parseJsonPath(query);
const maxVisitedNodes = options.maxVisitedNodes ?? 5e4;
const maxResults = options.maxResults ?? 5e3;
if (maxVisitedNodes <= 0 || maxResults <= 0)
throw new RuleExecutionError(
"security_violation",
"invalid JSONPath limits"
);
let visitedNodes = 0;
const visit = () => {
assertActive$1(options.signal);
visitedNodes += 1;
if (visitedNodes > maxVisitedNodes)
throw new RuleExecutionError(
"budget_exceeded",
"JSONPath node budget exceeded"
);
};
const enforceResults = (results) => {
if (results.length > maxResults)
throw new RuleExecutionError(
"budget_exceeded",
"JSONPath result limit exceeded"
);
return results;
};
let current = [value];
for (const token of tokens) {
const next = [];
for (const candidate of current) {
visit();
if (token.type === "property") {
const property = ownDataProperty(candidate, token.key);
if (property !== void 0) next.push(property);
} else if (token.type === "index") {
if (Array.isArray(candidate) && token.index < candidate.length)
next.push(ownDataProperty(candidate, String(token.index)));
} else if (token.type === "wildcard") {
next.push(...enumerableChildren(candidate));
} else {
const seen = /* @__PURE__ */ new WeakSet();
const walk = (node) => {
visit();
if (!node || typeof node !== "object" || seen.has(node)) return;
seen.add(node);
const property = ownDataProperty(node, token.key);
if (property !== void 0) {
next.push(property);
enforceResults(next);
}
for (const child of enumerableChildren(node)) walk(child);
};
walk(candidate);
}
enforceResults(next);
}
current = next;
}
return enforceResults(current);
}
const createReference = (kind) => Object.freeze({
kind,
toJSON() {
throw new Error("runtime reference cannot be serialized");
}
});
class RuntimeReferenceRegistry {
constructor(options) {
__publicField(this, "domValues", /* @__PURE__ */ new WeakMap());
__publicField(this, "domRefs", /* @__PURE__ */ new WeakMap());
__publicField(this, "frameValues", /* @__PURE__ */ new WeakMap());
__publicField(this, "frameRefs", /* @__PURE__ */ new WeakMap());
__publicField(this, "responseValues", /* @__PURE__ */ new WeakMap());
__publicField(this, "domRefCount", 0);
__publicField(this, "disposed", false);
__publicField(this, "maxDomRefs");
this.options = options;
if (!Number.isInteger(options.maxDomRefs) || options.maxDomRefs <= 0)
throw new Error("invalid DOM ref limit");
this.maxDomRefs = options.maxDomRefs;
}
/**
* 收紧 DOM 引用上限。registry 在变体解析之前就要建好,而 variant.limits 是解析后才知道的,
* 所以这里补一次收紧;只许调小,规则不能借此放大 policy 给的预算。
*/
tightenDomRefCap(limit) {
if (limit == null || !Number.isInteger(limit) || limit <= 0) return;
if (limit < this.maxDomRefs) this.maxDomRefs = limit;
}
createDomRef(element) {
this.assertActive();
const existing = this.domRefs.get(element);
if (existing) return existing;
if (this.domRefCount >= this.maxDomRefs)
throw new RuleExecutionError(
"budget_exceeded",
"DOM reference limit exceeded"
);
const reference = createReference("dom-ref");
this.domValues.set(reference, element);
this.domRefs.set(element, reference);
this.domRefCount += 1;
return reference;
}
createFrameRef(document2) {
this.assertActive();
const existing = this.frameRefs.get(document2);
if (existing) return existing;
const reference = createReference("frame-ref");
this.frameValues.set(reference, document2);
this.frameRefs.set(document2, reference);
return reference;
}
createResponseRef(response) {
this.assertActive();
const reference = createReference("response-ref");
this.responseValues.set(reference, response);
return reference;
}
getDom(reference) {
return this.resolve(reference, this.domValues, "DOM");
}
getFrame(reference) {
return this.resolve(reference, this.frameValues, "frame");
}
getResponse(reference) {
return this.resolve(reference, this.responseValues, "response");
}
dispose() {
this.disposed = true;
}
assertActive() {
if (this.disposed)
throw new RuleExecutionError(
"security_violation",
"runtime reference registry is disposed"
);
}
resolve(reference, values, label) {
this.assertActive();
if (reference == null)
throw new RuleDomainError(
"missing_reference",
`missing ${label} reference`
);
if (typeof reference !== "object" && typeof reference !== "function")
throw new RuleExecutionError(
"security_violation",
`invalid ${label} reference`
);
if (!values.has(reference))
throw new RuleExecutionError(
"security_violation",
`invalid ${label} reference`
);
return values.get(reference);
}
}
class RuleResourceScope {
constructor() {
__publicField(this, "cleanups", []);
__publicField(this, "disposePromise", null);
__publicField(this, "resolveDisposed");
__publicField(this, "disposed", new Promise((resolve) => {
this.resolveDisposed = resolve;
}));
}
add(cleanup) {
if (this.disposePromise)
throw new Error("rule resource scope is already disposed");
this.cleanups.push(cleanup);
}
bind(signal) {
if (!signal) return;
if (signal.aborted) {
void this.dispose();
return;
}
const onAbort = () => void this.dispose();
signal.addEventListener("abort", onAbort, { once: true });
this.add(() => signal.removeEventListener("abort", onAbort));
}
dispose() {
if (this.disposePromise) return this.disposePromise;
this.disposePromise = (async () => {
for (const cleanup of this.cleanups.reverse()) {
try {
await cleanup();
} catch {
}
}
this.cleanups.length = 0;
this.resolveDisposed();
})();
return this.disposePromise;
}
}
const recordFromMap = (values) => {
const record = /* @__PURE__ */ Object.create(null);
for (const [name, value] of values) record[name] = value;
return record;
};
const userVariables = (result) => new Map([...result.variables].filter(([name]) => !name.startsWith("$")));
class RuleStateMachine {
constructor(options) {
__publicField(this, "variables");
__publicField(this, "started", false);
__publicField(this, "transitions", 0);
__publicField(this, "queue", Promise.resolve());
__publicField(this, "maxTransitions");
__publicField(this, "state");
this.options = options;
this.state = options.definition.initial;
if (!options.definition.states[this.state])
throw new RuleExecutionError(
"security_violation",
"state machine initial state does not exist"
);
this.variables = new Map(Object.entries(options.variables ?? {}));
this.maxTransitions = options.maxTransitions ?? 1e4;
if (!Number.isInteger(this.maxTransitions) || this.maxTransitions <= 0 || this.maxTransitions > 1e4)
throw new RuleExecutionError(
"security_violation",
"invalid state transition limit"
);
}
getVariable(name) {
return this.variables.get(name);
}
start() {
return this.enqueue(async () => {
if (this.started) return { state: this.state };
const initial = this.options.definition.states[this.state];
if (!initial)
throw new RuleExecutionError(
"security_violation",
"state machine initial state does not exist"
);
const working = await this.runSteps(
initial.enter ?? [],
this.variables,
null,
`state:${this.state}:enter`
);
this.variables = working;
this.started = true;
return { state: this.state };
});
}
dispatch(event, payload = null) {
return this.enqueue(async () => {
if (!this.started)
throw new RuleDomainError(
"state_machine_not_started",
"state machine is not started"
);
const source = this.options.definition.states[this.state];
if (!source)
throw new RuleExecutionError(
"security_violation",
`state does not exist: ${this.state}`
);
let selected2;
for (const transition of source.transitions) {
if (transition.event !== event) continue;
if (!transition.when) {
selected2 = transition;
break;
}
const matches = await this.evaluateCondition(
transition.when,
this.variables,
payload
);
if (matches) {
selected2 = transition;
break;
}
}
if (!selected2) return { state: this.state, transitioned: false };
if (this.transitions >= this.maxTransitions)
throw new RuleExecutionError(
"budget_exceeded",
"state transition budget exceeded"
);
const target = this.options.definition.states[selected2.target];
if (!target)
throw new RuleExecutionError(
"security_violation",
`state transition target does not exist: ${selected2.target}`
);
let working = new Map(this.variables);
working = await this.runSteps(
selected2.actions ?? [],
working,
payload,
`state:${this.state}:${event}:actions`
);
working = await this.runSteps(
target.enter ?? [],
working,
payload,
`state:${selected2.target}:enter`
);
this.variables = working;
this.state = selected2.target;
this.transitions += 1;
return { state: this.state, transitioned: true };
});
}
async evaluateCondition(when, variables, payload) {
const result = await this.runFlow(
{
id: `state:${this.state}:condition`,
steps: [{ type: "return", value: when }]
},
variables,
payload
);
if (typeof result.value !== "boolean")
throw new RuleDomainError(
"invalid_type",
"state transition condition must be boolean"
);
return result.value;
}
async runSteps(steps, variables, payload, flowId) {
if (steps.length === 0) return new Map(variables);
return userVariables(
await this.runFlow({ id: flowId, steps: [...steps] }, variables, payload)
);
}
runFlow(flow, variables, payload) {
return this.options.interpreter.run(flow, {
phase: "lifecycle",
variables: recordFromMap(variables),
reserved: { ...this.options.reserved ?? {}, $event: payload },
flows: this.options.flows,
capabilities: this.options.capabilities,
signal: this.options.signal,
resources: this.options.resources,
limits: this.options.limits
});
}
enqueue(operation) {
const task = this.queue.then(operation);
this.queue = task.catch(() => void 0);
return task;
}
}
function operationsEqual(left, right) {
if (left.kind !== right.kind) return false;
if (left.kind === "choose" && right.kind === "choose")
return left.optionId === right.optionId;
if (left.kind === "write" && right.kind === "write")
return left.slotId === right.slotId && left.value === right.value;
return left.kind === "pair" && right.kind === "pair" && left.leftId === right.leftId && left.rightId === right.rightId;
}
function assertActive(signal) {
if (signal.aborted)
throw new RuleExecutionError("cancelled", "answer write was cancelled");
}
class BindingRegistryAnswerWriter {
constructor(registry) {
this.registry = registry;
}
async applyPlan(plan, signal) {
const resolved = this.resolveTargets(plan, plan.operations, signal);
if (!resolved) return false;
const applied = [];
try {
for (const { operation, target } of resolved) {
assertActive(signal);
if (!await target.apply(operation, signal)) {
await this.revertApplied(applied, signal);
return false;
}
applied.push(target);
}
return true;
} catch (error) {
await this.revertApplied(applied, signal);
throw error;
}
}
/** 撤销本身也可能失败(元素已经没了、target 压根撤不掉),不能因此盖住原始失败。 */
async revertApplied(applied, signal) {
var _a2;
for (const target of [...applied].reverse()) {
try {
await ((_a2 = target.revert) == null ? void 0 : _a2.call(target, signal));
} catch {
}
}
}
async verifyPlan(plan, signal) {
const resolved = this.resolveTargets(plan, plan.operations, signal);
if (!resolved) return false;
for (const { operation, target } of resolved) {
assertActive(signal);
if (!await target.verify(operation, signal)) return false;
}
return true;
}
async applyOperation(plan, operation, signal) {
if (!plan.operations.some(
(candidate) => operationsEqual(candidate, operation)
))
return false;
const resolved = this.resolveTargets(plan, [operation], signal);
if (!resolved) return false;
return resolved[0] ? resolved[0].target.apply(operation, signal) : false;
}
async verifyOperation(plan, operation, signal) {
if (!plan.operations.some(
(candidate) => operationsEqual(candidate, operation)
))
return false;
const resolved = this.resolveTargets(plan, [operation], signal);
if (!resolved) return false;
return resolved[0] ? resolved[0].target.verify(operation, signal) : false;
}
resolveTargets(plan, operations, signal) {
assertActive(signal);
const binding = this.registry.get(plan.path);
if (!binding || !binding.connected || binding.capturedFingerprint !== plan.fingerprint || binding.currentFingerprint !== plan.fingerprint)
return null;
const resolved = [];
for (const operation of operations) {
const target = this.registry.targetForOperation(plan.path, operation);
if (!target) return null;
try {
if (!target.isConnected()) return null;
} catch {
return null;
}
resolved.push({ operation, target });
}
return resolved;
}
}
class RuleVerificationError extends Error {
constructor(code, message) {
super(message);
this.code = code;
this.name = "RuleVerificationError";
}
}
function compareRuleVersions(leftValue, rightValue) {
try {
return compareRuleVersions$1(leftValue, rightValue);
} catch {
throw new RuleVerificationError(
"engine_incompatible",
`invalid version: ${leftValue} or ${rightValue}`
);
}
}
function nestedSteps(step) {
switch (step.type) {
case "if":
return [step.then, step.else ?? []];
case "switch":
return [...step.cases.map((item) => item.steps), step.default ?? []];
case "forEach":
case "while":
return [step.steps];
case "try":
return [step.steps, step.catch ?? [], step.finally ?? []];
default:
return [];
}
}
function visitSteps(steps, visitor) {
for (const step of steps) {
visitor(step);
for (const nested of nestedSteps(step)) visitSteps(nested, visitor);
}
}
function stepExpressions(step) {
switch (step.type) {
case "set":
case "switch":
return [step.value];
case "if":
case "while":
return [step.when];
case "forEach":
return [step.items];
case "callFlow":
case "primitive":
return Object.values(step.args ?? {});
case "return":
return step.value ? [step.value] : [];
default:
return [];
}
}
function childExpressions(expr) {
switch (expr.op) {
case "path":
case "jsonPath":
return [expr.from];
case "coalesce":
case "logic":
return expr.values;
case "array":
return expr.items;
case "object":
case "format":
return Object.values(expr.op === "object" ? expr.entries : expr.args);
case "compare":
return [expr.left, expr.right];
case "not":
case "string":
case "regex":
return [expr.value];
case "map":
return [expr.items, expr.value];
case "filter":
return [expr.items, expr.when];
case "reduce":
return [expr.items, expr.initial, expr.value];
default:
return [];
}
}
function visitExpressions(expr, visitor) {
visitor(expr);
for (const child of childExpressions(expr)) visitExpressions(child, visitor);
}
function lifecycleSteps(lifecycle) {
const result = [];
for (const state of Object.values(lifecycle.states)) {
result.push(state.enter ?? []);
for (const transition of state.transitions)
result.push(transition.actions ?? []);
}
return result;
}
const LIMIT_KEYS = [
"maxSteps",
"maxWallMs",
"maxAsyncMs",
"maxLoopIterations",
"maxCallDepth",
"maxDomRefs"
];
class RuleVerifier {
constructor(options) {
__publicField(this, "now");
__publicField(this, "keyset");
this.options = options;
this.now = options.now ?? Date.now;
this.keyset = ServerKeysetSchema.parse(options.keyset);
}
async verify(input) {
const pkg = RulePackageSchema.parse(input);
const now = this.now();
if (pkg.issuedAt > now)
throw new RuleVerificationError(
"package_from_future",
"rule package is issued in the future"
);
if (pkg.expiresAt != null && pkg.expiresAt <= now)
throw new RuleVerificationError(
"package_expired",
"rule package is expired"
);
this.verifyEngineRange(pkg);
this.verifyCapabilitiesAndLimits(pkg);
this.verifyPrimitives(pkg);
const contentHash = await computeRulePackageContentHash(pkg);
if (contentHash !== pkg.contentHash)
throw new RuleVerificationError(
"content_hash_mismatch",
"rule package content hash mismatch"
);
const signingKey = this.keyset.keys.find(
(key) => key.kid === pkg.signingKid && key.use === "rule-signing" && key.notBefore <= now && key.expiresAt > now && this.keyset.issuedAt <= now && this.keyset.expiresAt > now
);
if (!signingKey)
throw new RuleVerificationError(
"invalid_signing_key",
"active rule-signing key not found"
);
const publicKey = await importEcdsaPublicJwk(signingKey.publicJwk);
if (!await verifyEcdsaP1363(
publicKey,
utf8Bytes(rulePackageSignatureInput(pkg)),
pkg.signature
))
throw new RuleVerificationError(
"invalid_signature",
"rule package signature rejected"
);
await this.verifySequenceAndRollback(pkg);
return pkg;
}
verifyEngineRange(pkg) {
if (compareRuleVersions(this.options.engineVersion, pkg.engineRange.min) < 0 || pkg.engineRange.maxExclusive != null && compareRuleVersions(
this.options.engineVersion,
pkg.engineRange.maxExclusive
) >= 0)
throw new RuleVerificationError(
"engine_incompatible",
"rule package engine range is incompatible"
);
}
verifyCapabilitiesAndLimits(pkg) {
var _a2;
for (const capability of pkg.capabilities) {
if (!this.options.policy.capabilities.has(capability))
throw new RuleVerificationError(
"capability_denied",
`rule capability is not allowed: ${capability}`
);
}
for (const variant of pkg.variants) {
for (const key of LIMIT_KEYS) {
const value = (_a2 = variant.limits) == null ? void 0 : _a2[key];
if (value != null && value > this.options.policy.limits[key])
throw new RuleVerificationError(
"limit_denied",
`rule limit exceeds runtime policy: ${key}`
);
}
}
}
verifyPrimitives(pkg) {
const capabilities = new Set(pkg.capabilities);
const verifyFlow = (flow, phase) => this.verifyStepList(flow.steps, phase, capabilities);
for (const variant of pkg.variants) {
verifyFlow(variant.match, "match");
verifyFlow(variant.capture, "capture");
verifyFlow(variant.fill, "fill");
if (variant.diagnostics) verifyFlow(variant.diagnostics, "diagnostic");
if (variant.lifecycle) {
for (const steps of lifecycleSteps(variant.lifecycle))
this.verifyStepList(steps, "lifecycle", capabilities);
for (const state of Object.values(variant.lifecycle.states))
for (const transition of state.transitions) {
if (!RULE_DISPATCHED_EVENTS.has(transition.event))
throw new RuleVerificationError(
"event_denied",
`engine never dispatches lifecycle event: ${transition.event}`
);
if (transition.when) this.verifyExpression(transition.when);
}
}
}
}
verifyStepList(steps, phase, capabilities) {
visitSteps(steps, (step) => {
for (const expression of stepExpressions(step))
this.verifyExpression(expression);
if (step.type !== "primitive") return;
const definition = this.options.registry.get(step.id);
if (!definition)
throw new RuleVerificationError(
"unknown_primitive",
`unknown primitive: ${step.id}`
);
if (!this.options.policy.primitives.has(step.id))
throw new RuleVerificationError(
"primitive_denied",
`primitive is not allowed by runtime policy: ${step.id}`
);
if (!definition.phases.includes(phase))
throw new RuleVerificationError(
"primitive_phase_denied",
`primitive is not allowed in ${phase}: ${step.id}`
);
if (definition.capability && (!this.options.policy.capabilities.has(definition.capability) || !capabilities.has(definition.capability)))
throw new RuleVerificationError(
"capability_denied",
`primitive capability is not declared: ${definition.capability}`
);
});
}
/** 表达式求值器是运行时注入的:验签期就拒掉本机跑不动的表达式,否则规则会在页面上静默失败。 */
verifyExpression(expr) {
visitExpressions(expr, (node) => {
var _a2, _b;
if (node.op === "regex" && !((_a2 = this.options.services) == null ? void 0 : _a2.regex))
throw new RuleVerificationError(
"expression_denied",
"regex executor is not installed"
);
if (node.op === "jsonPath" && !((_b = this.options.services) == null ? void 0 : _b.jsonPath))
throw new RuleVerificationError(
"expression_denied",
"JSONPath executor is not installed"
);
});
}
async verifySequenceAndRollback(pkg) {
const current = this.options.current;
if (!current) return;
if (pkg.releaseSequence < current.releaseSequence)
throw new RuleVerificationError(
"sequence_downgrade",
"rule release sequence cannot decrease"
);
if (pkg.releaseSequence === current.releaseSequence && pkg.contentHash !== current.contentHash)
throw new RuleVerificationError(
"sequence_reuse",
"rule release sequence cannot be reused for different content"
);
const isVersionRollback = compareRuleVersions(pkg.version, current.version) < 0;
if (!isVersionRollback && !pkg.rollbackAuthorization) return;
const authorization = pkg.rollbackAuthorization;
if (!authorization || authorization.toVersion !== pkg.version || !this.options.authorizeRollback || !await this.options.authorizeRollback(authorization, pkg))
throw new RuleVerificationError(
"rollback_unauthorized",
"rule rollback is not authorized"
);
}
}
class RuleStoreError extends Error {
constructor(code, message) {
super(message);
this.code = code;
this.name = "RuleStoreError";
}
}
function freezeJson(value) {
if (!value || typeof value !== "object" || Object.isFrozen(value))
return value;
for (const child of Object.values(value)) freezeJson(child);
return Object.freeze(value);
}
const emptyState = () => ({
candidate: null,
active: null,
lastKnownGood: null,
highestSequence: -1,
highestHash: null,
disabled: false,
quarantined: []
});
class RuleStore {
constructor() {
__publicField(this, "states", /* @__PURE__ */ new Map());
}
/**
* 同步直装一个已经信得过的包为 active,**不做验签、不动防回退水位**。
*
* 生产链路一律走 `stageRemote` + `activateCandidate`(验签、序号闸、LKG 都在那条
* 路上);这条只服务「包已经在手上」的场景——夹具装载。它取代的是原先的
* `installBundled`:那个方法把包装进一个**第三档**(active → LKG → bundled),
* 而公开脚本自 2026-08-01 起不携带任何规则主体,那一档从此没有生产写入口,
* 只剩下每个读者都要多理解一层、每个消费者都要多处理一个来源。档删了,
* 直装留下。
*/
install(input) {
const pkg = freezeJson(RulePackageSchema.parse(input));
this.state(pkg.packageId).active = pkg;
return pkg;
}
async stageRemote(input, verifier) {
const pkg = freezeJson(
RulePackageSchema.parse(await verifier.verify(input))
);
const state = this.state(pkg.packageId);
if (pkg.releaseSequence < state.highestSequence)
throw new RuleStoreError(
"sequence_downgrade",
"rule release sequence cannot decrease"
);
if (pkg.releaseSequence === state.highestSequence && state.highestHash !== pkg.contentHash)
throw new RuleStoreError(
"sequence_reuse",
"rule release sequence cannot be reused for different content"
);
if (pkg.releaseSequence === state.highestSequence && state.highestHash === pkg.contentHash) {
return state.candidate ?? state.active ?? state.lastKnownGood ?? pkg;
}
state.candidate = pkg;
state.highestSequence = pkg.releaseSequence;
state.highestHash = pkg.contentHash;
return pkg;
}
activateCandidate(packageId) {
const state = this.state(packageId);
if (!state.candidate)
throw new RuleStoreError(
"candidate_missing",
`rule candidate is missing: ${packageId}`
);
if (state.active) state.lastKnownGood = state.active;
state.active = state.candidate;
state.candidate = null;
return state.active;
}
quarantineActive(packageId, reason) {
const state = this.state(packageId);
if (!state.active) return;
state.quarantined.push({
pkg: state.active,
reason
});
state.active = null;
}
setDisabled(packageId, disabled) {
this.state(packageId).disabled = disabled;
}
resolve(packageId) {
const state = this.state(packageId);
if (state.disabled) return null;
if (state.active) return { source: "remote-active", pkg: state.active };
if (state.lastKnownGood)
return { source: "remote-lkg", pkg: state.lastKnownGood };
return null;
}
diagnostics(packageId) {
const state = this.state(packageId);
return {
packageId,
disabled: state.disabled,
candidate: state.candidate,
active: state.active,
lastKnownGood: state.lastKnownGood,
highestSequence: state.highestSequence,
quarantined: [...state.quarantined]
};
}
exportSnapshot() {
return {
schemaVersion: 1,
packages: [...this.states.entries()].filter(
([, state]) => state.disabled || state.candidate != null || state.active != null || state.lastKnownGood != null || state.quarantined.length > 0 || // 只剩防回退水位的降级条目也要落盘,否则下次启动水位归零
state.highestSequence >= 0
).map(([packageId, state]) => ({
packageId,
disabled: state.disabled,
candidate: state.candidate,
active: state.active,
lastKnownGood: state.lastKnownGood,
highestSequence: state.highestSequence,
highestHash: state.highestHash,
quarantined: [...state.quarantined]
}))
};
}
async restoreSnapshot(input, verifier) {
if (!input || typeof input !== "object")
throw this.snapshotError("rule snapshot must be an object");
const snapshot2 = input;
if (snapshot2.schemaVersion !== 1 || !Array.isArray(snapshot2.packages))
throw this.snapshotError("unsupported rule snapshot");
if (snapshot2.packages.length > 128)
throw this.snapshotError("rule snapshot contains too many packages");
const restored = /* @__PURE__ */ new Map();
for (const rawEntry of snapshot2.packages) {
if (!rawEntry || typeof rawEntry !== "object")
throw this.snapshotError("invalid rule snapshot entry");
const entry = rawEntry;
if (typeof entry.packageId !== "string" || entry.packageId.length === 0 || typeof entry.disabled !== "boolean" || !Number.isInteger(entry.highestSequence) || entry.highestHash !== null && typeof entry.highestHash !== "string" || !Array.isArray(entry.quarantined))
throw this.snapshotError("invalid rule snapshot metadata");
if (restored.has(entry.packageId))
throw this.snapshotError("duplicate rule snapshot package");
const verifyPackage = async (value) => {
if (value == null) return null;
const pkg = freezeJson(
RulePackageSchema.parse(await verifier.verify(value))
);
if (pkg.packageId !== entry.packageId)
throw this.snapshotError("rule snapshot packageId mismatch");
return pkg;
};
const packageId = entry.packageId;
const degraded = () => ({
candidate: null,
active: null,
lastKnownGood: null,
// 水位必须留下:否则一个被撤下的旧签名包能重新通过序号闸
highestSequence: entry.highestSequence,
highestHash: entry.highestHash,
disabled: entry.disabled,
quarantined: []
});
let candidate;
let active2;
let lastKnownGood;
try {
;
[candidate, active2, lastKnownGood] = await Promise.all([
verifyPackage(entry.candidate),
verifyPackage(entry.active),
verifyPackage(entry.lastKnownGood)
]);
} catch (error) {
if (!(error instanceof RuleVerificationError)) throw error;
restored.set(packageId, degraded());
continue;
}
const quarantined = [];
for (const rawQuarantine of entry.quarantined) {
if (!rawQuarantine || typeof rawQuarantine !== "object")
throw this.snapshotError("invalid quarantined rule");
const quarantine = rawQuarantine;
if (typeof quarantine.reason !== "string" || quarantine.reason.length > 512)
throw this.snapshotError("invalid quarantine reason");
const quarantinedPackage = await verifyPackage(quarantine.pkg);
if (!quarantinedPackage)
throw this.snapshotError("missing quarantined package");
quarantined.push({
pkg: quarantinedPackage,
reason: quarantine.reason
});
}
const remotePackages = [
candidate,
active2,
lastKnownGood,
...quarantined.map((item) => item.pkg)
].filter((pkg) => pkg != null);
const highestSequence = entry.highestSequence;
const highestHash = entry.highestHash;
if (remotePackages.length === 0) {
if (highestSequence < -1)
throw this.snapshotError("empty snapshot has invalid sequence");
} else if (!remotePackages.some(
(pkg) => pkg.releaseSequence === highestSequence && pkg.contentHash === highestHash
) || remotePackages.some((pkg) => pkg.releaseSequence > highestSequence))
throw this.snapshotError("rule snapshot highest sequence mismatch");
restored.set(entry.packageId, {
candidate,
active: active2,
lastKnownGood,
highestSequence,
highestHash,
disabled: entry.disabled,
quarantined
});
}
for (const [packageId, state] of restored) this.states.set(packageId, state);
}
snapshotError(message) {
return new RuleStoreError("snapshot_invalid", message);
}
state(packageId) {
let state = this.states.get(packageId);
if (!state) {
state = emptyState();
this.states.set(packageId, state);
}
return state;
}
}
class JsonRuleResolver {
constructor(options) {
this.options = options;
}
async resolve(packageId, options = {}) {
var _a2, _b, _c, _d, _e, _f;
const resolved = this.options.store.resolve(packageId);
if (!resolved) return null;
const variants = resolved.pkg.variants.map((variant, index) => ({ variant, index })).sort(
(left, right) => right.variant.priority - left.variant.priority || left.index - right.index
);
for (const { variant } of variants) {
try {
const result = await this.options.interpreter.run(variant.match, {
phase: "match",
variables: options.variables,
reserved: options.reserved,
signal: options.signal,
capabilities: new Set(resolved.pkg.capabilities),
limits: variant.limits,
flows: [
variant.capture,
variant.fill,
...variant.diagnostics ? [variant.diagnostics] : []
]
});
if (typeof result.value !== "boolean") {
(_b = (_a2 = this.options).onAttempt) == null ? void 0 : _b.call(_a2, {
variantId: variant.id,
matched: false,
reason: "invalid_match_result",
steps: result.steps
});
continue;
}
(_d = (_c = this.options).onAttempt) == null ? void 0 : _d.call(_c, {
variantId: variant.id,
matched: result.value,
steps: result.steps
});
if (result.value)
return { source: resolved.source, pkg: resolved.pkg, variant };
} catch (error) {
if (!(error instanceof RuleDomainError)) throw error;
(_f = (_e = this.options).onAttempt) == null ? void 0 : _f.call(_e, {
variantId: variant.id,
matched: false,
reason: error.code
});
}
}
return null;
}
}
function registerLocalHook(registry, hook) {
registry.register({
id: hook.id,
phases: hook.phases,
capability: hook.capability,
requiresSafetyCapability: hook.requiresSafetyCapability,
safetyArgument: hook.safetyArgument,
execute: async ({ args, ...context }) => {
let parsed;
try {
parsed = hook.parseArgs(args);
} catch (error) {
throw new RuleDomainError(
"invalid_hook_args",
error instanceof Error ? error.message : "invalid hook arguments"
);
}
const result = await hook.execute(parsed, context);
if (!hook.validateResult(result))
throw new RuleDomainError(
"invalid_hook_result",
`local hook returned an invalid result: ${hook.id}`
);
return result;
}
});
}
const FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
const MAX_FIELDS = 64;
const MAX_PATH_DEPTH = 32;
const MAX_SNAPSHOT_NODES = 1e4;
const MAX_SNAPSHOT_BYTES = 512 * 1024;
const utf8Length$1 = (value) => new TextEncoder().encode(value).length;
function createSnapshotState(signal, label) {
return { signal, nodes: 0, bytes: 0, ancestors: /* @__PURE__ */ new WeakSet(), label };
}
function safeKey(value) {
return typeof value === "string" && value.length > 0 && value.length <= 256 && !FORBIDDEN_KEYS.has(value);
}
function parseSnapshotFields(value) {
if (!value || typeof value !== "object" || Array.isArray(value))
throw new Error("fields must be an object");
const entries = Object.entries(value);
if (entries.length === 0 || entries.length > MAX_FIELDS)
throw new Error("fields must contain between 1 and 64 entries");
const fields = /* @__PURE__ */ Object.create(null);
for (const [name, rawPath] of entries) {
if (!safeKey(name)) throw new Error(`invalid field name: ${name}`);
if (!Array.isArray(rawPath) || rawPath.length === 0 || rawPath.length > MAX_PATH_DEPTH)
throw new Error(`invalid snapshot path: ${name}`);
fields[name] = rawPath.map((segment) => {
if (typeof segment === "number") {
if (!Number.isInteger(segment) || segment < 0)
throw new Error(`invalid snapshot path index: ${name}`);
return segment;
}
if (!safeKey(segment))
throw new Error(`invalid snapshot path property: ${name}`);
return segment;
});
}
return fields;
}
function consumeSnapshotValue(state, value) {
if (state.signal.aborted)
throw new RuleExecutionError(
"cancelled",
`${state.label} snapshot cancelled`
);
state.nodes += 1;
if (value) state.bytes += utf8Length$1(value);
if (state.nodes > MAX_SNAPSHOT_NODES || state.bytes > MAX_SNAPSHOT_BYTES)
throw new RuleExecutionError(
"budget_exceeded",
`${state.label} snapshot budget exceeded`
);
}
function cloneJsonValue(value, state) {
if (value === null || typeof value === "boolean") {
consumeSnapshotValue(state);
return value;
}
if (typeof value === "number") {
consumeSnapshotValue(state);
if (!Number.isFinite(value))
throw new RuleDomainError(
"scope_value_unsafe",
`${state.label} contains a non-finite number`
);
return value;
}
if (typeof value === "string") {
consumeSnapshotValue(state, value);
return value;
}
if (!value || typeof value !== "object")
throw new RuleDomainError(
"scope_value_unsafe",
`${state.label} field is not JSON data`
);
consumeSnapshotValue(state);
if (state.ancestors.has(value))
throw new RuleDomainError(
"scope_value_unsafe",
`${state.label} field contains a cycle`
);
state.ancestors.add(value);
try {
if (Array.isArray(value)) {
if (value.length > MAX_SNAPSHOT_NODES)
throw new RuleExecutionError(
"budget_exceeded",
`${state.label} array exceeds snapshot budget`
);
return Array.from({ length: value.length }, (_, index) => {
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
if (!descriptor) {
consumeSnapshotValue(state);
return null;
}
if (!("value" in descriptor))
throw new RuleDomainError(
"scope_value_unsafe",
`${state.label} array contains an accessor`
);
return cloneJsonValue(descriptor.value, state);
});
}
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null)
throw new RuleDomainError(
"scope_value_unsafe",
`${state.label} field must be plain JSON data`
);
const result = /* @__PURE__ */ Object.create(null);
for (const key of Object.keys(value)) {
if (!safeKey(key))
throw new RuleDomainError(
"scope_value_unsafe",
`${state.label} contains an unsafe key: ${key}`
);
consumeSnapshotValue(state, key);
const descriptor = Object.getOwnPropertyDescriptor(value, key);
if (!descriptor || !("value" in descriptor))
throw new RuleDomainError(
"scope_value_unsafe",
`${state.label} contains an accessor: ${key}`
);
result[key] = cloneJsonValue(descriptor.value, state);
}
return result;
} finally {
state.ancestors.delete(value);
}
}
const MAX_UEDITOR_TARGETS = 64;
const utf8Length = (value) => new TextEncoder().encode(value).length;
function registerChaoxingRuleHooks(registry, dependencies) {
registerLocalHook(registry, {
id: "chaoxing.normalizeTitle",
phases: ["capture", "diagnostic"],
parseArgs: (args) => {
if (typeof args.text !== "string") throw new Error("text is required");
if (utf8Length(args.text) > 128 * 1024)
throw new Error("title hook input exceeds byte limit");
return { text: args.text };
},
validateResult: (value) => typeof value === "string",
execute: ({ text }) => stripTitle(text)
});
registerLocalHook(registry, {
id: "chaoxing.decodeFont",
phases: ["capture", "diagnostic"],
capability: "runtime-read",
parseArgs: (args) => {
if (typeof args.text !== "string" || typeof args.styleText !== "string")
throw new Error("text and styleText are required");
if (utf8Length(args.text) > 128 * 1024 || utf8Length(args.styleText) > 2 * 1024 * 1024)
throw new Error("font hook input exceeds byte limit");
return { text: args.text, styleText: args.styleText };
},
validateResult: (value) => typeof value === "string",
execute: ({ text, styleText }) => {
const base64 = extractCxFontBase64(styleText);
if (!base64)
throw new RuleDomainError(
"font_data_missing",
"Chaoxing font data is missing"
);
if (Object.keys(dependencies.table).length === 0)
throw new RuleDomainError(
"font_table_unavailable",
"Chaoxing font table is unavailable"
);
let fontData;
try {
fontData = base64ToUint8Array(base64);
} catch {
throw new RuleDomainError(
"font_data_invalid",
"Chaoxing font data is invalid"
);
}
return applyCharMap(
text,
buildCharMap(fontData, dependencies.table, dependencies.typr)
);
}
});
registerLocalHook(registry, {
id: "chaoxing.harvestAnswerValues",
phases: ["capture", "diagnostic"],
parseArgs: (args) => {
if (typeof args.text !== "string") throw new Error("text is required");
if (utf8Length(args.text) > 64 * 1024)
throw new Error("harvest hook input exceeds byte limit");
const options = args.options ?? [];
if (!Array.isArray(options) || options.length > 64)
throw new Error("options must be a bounded option array");
const contents = options.map((option) => {
const content = option == null ? void 0 : option.content;
if (typeof content !== "string")
throw new Error("option content must be a string");
return content;
});
const slotValues = args.slotValues ?? [];
if (!Array.isArray(slotValues) || slotValues.length > 64)
throw new Error("slotValues must be a bounded string array");
return {
text: args.text,
options: contents,
slotValues: slotValues.map((value) => String(value))
};
},
validateResult: (value) => Array.isArray(value),
execute: ({ text, options, slotValues }) => mapChaoxingHarvestedAnswer(text, options, slotValues)
});
if (dependencies.refs && dependencies.resolveUeditorBodies) {
const { refs, resolveUeditorBodies: resolveUeditorBodies2 } = dependencies;
registerLocalHook(registry, {
id: "chaoxing.ueditorBodies",
phases: ["capture", "diagnostic"],
capability: "runtime-read",
parseArgs: (args) => {
if (!Array.isArray(args.targets) || args.targets.length === 0 || args.targets.length > MAX_UEDITOR_TARGETS)
throw new Error(
"targets must contain between 1 and 64 DOM references"
);
return { targets: args.targets };
},
validateResult: (value) => Array.isArray(value),
execute: ({ targets }) => {
const textareas = targets.map((target) => {
const element = refs.getDom(target);
if (element.tagName.toLowerCase() !== "textarea")
throw new RuleDomainError(
"ueditor_target_invalid",
"Chaoxing UEditor source target must be a textarea"
);
return element;
});
let bodies;
try {
bodies = resolveUeditorBodies2(textareas);
} catch (error) {
throw new RuleDomainError(
"ueditor_body_unavailable",
error instanceof Error ? error.message : "Chaoxing UEditor body is unavailable"
);
}
if (!Array.isArray(bodies) || bodies.length !== textareas.length)
throw new RuleDomainError(
"ueditor_body_unavailable",
"Chaoxing UEditor body count does not match source targets"
);
return bodies.map((body) => {
var _a2;
if (!body || !body.isConnected || ((_a2 = body.getAttribute("contenteditable")) == null ? void 0 : _a2.toLowerCase()) !== "true")
throw new RuleDomainError(
"ueditor_body_unavailable",
"Chaoxing UEditor body must be a connected contenteditable target"
);
return refs.createDomRef(body);
});
}
});
}
if (dependencies.refs && dependencies.registerExamQuestion) {
const { refs, registerExamQuestion } = dependencies;
registerLocalHook(registry, {
id: "chaoxing.examRegisterQuestion",
phases: ["capture", "diagnostic"],
capability: "runtime-read",
parseArgs: (args) => {
if (typeof args.path !== "string" || !args.path.startsWith("/") || args.path.length > 1024)
throw new Error("path must be a valid question path");
const mode = args.mode;
if (mode !== "paged" && mode !== "preview")
throw new Error("mode must be paged or preview");
return {
path: args.path,
target: args.target,
mode
};
},
validateResult: (value) => typeof value === "string",
execute: ({ path, target, mode }) => {
const element = refs.getDom(target);
if (!element.isConnected || !element.classList.contains("questionLi"))
throw new RuleDomainError(
"exam_question_invalid",
"Chaoxing exam question target must be a connected questionLi"
);
registerExamQuestion({ path, target: element, mode });
return path;
}
});
}
const registerPlanHook = (id, execute) => registerLocalHook(registry, {
id,
phases: ["fill"],
capability: "answer-write",
requiresSafetyCapability: true,
parseArgs: (args) => ({ safety: args.safety }),
validateResult: (value) => typeof value === "boolean",
execute: ({ safety }, { signal }) => execute(safetyPlanForCapability(safety), signal)
});
if (dependencies.prepareExamPlan)
registerPlanHook("chaoxing.examPreparePlan", dependencies.prepareExamPlan);
if (dependencies.commitExamPlan)
registerPlanHook("chaoxing.examCommitPlan", dependencies.commitExamPlan);
if (dependencies.commitDoworkPlan)
registerPlanHook("chaoxing.doworkCommitPlan", dependencies.commitDoworkPlan);
if (dependencies.commitStudentstudyPlan)
registerPlanHook(
"chaoxing.studentstudyCommitPlan",
dependencies.commitStudentstudyPlan
);
if (dependencies.commitOldHomeworkPlan)
registerPlanHook(
"chaoxing.oldHomeworkCommitPlan",
dependencies.commitOldHomeworkPlan
);
if (dependencies.commitOldChapterPlan)
registerPlanHook(
"chaoxing.oldChapterCommitPlan",
dependencies.commitOldChapterPlan
);
if (dependencies.commitNewChapterPlan)
registerPlanHook(
"chaoxing.newChapterCommitPlan",
dependencies.commitNewChapterPlan
);
}
const LABEL = "aopeng paper data";
const AOPENG_PAPER_SLOTS = Object.freeze([
/** 考试查看态 `StudentViewPaper`:带 Answer.AnswerSheet / AnswerResult.MarkStatus。 */
"exam-view-paper",
/** 考试作答态 `StudentPullPaper_V2`:是否带 I7 未验证,先留槽不下结论。 */
"exam-pull-paper"
]);
const SLOT_SET = new Set(AOPENG_PAPER_SLOTS);
const MAX_EMBEDDED_JSON_DEPTH = 3;
function decodeEmbeddedJson(value) {
let current = value;
for (let depth = 0; depth < MAX_EMBEDDED_JSON_DEPTH; depth += 1) {
if (typeof current !== "string") return current;
const trimmed = current.trim();
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return current;
try {
current = JSON.parse(trimmed);
} catch {
return current;
}
}
return current;
}
function pathValueThroughEmbeddedJson(source, path) {
let current = decodeEmbeddedJson(source);
for (const segment of path) {
if (!current || typeof current !== "object") return null;
const descriptor = Object.getOwnPropertyDescriptor(current, String(segment));
if (!descriptor) return null;
if (!("value" in descriptor))
throw new RuleDomainError(
"scope_value_unsafe",
`${LABEL} path contains an accessor: ${String(segment)}`
);
current = decodeEmbeddedJson(descriptor.value);
}
return current;
}
function registerAopengRuleHooks(registry, environment) {
registerLocalHook(registry, {
id: "aopeng.paperData",
phases: ["capture", "diagnostic"],
capability: "network-read",
parseArgs: (args) => {
if (typeof args.slot !== "string" || !SLOT_SET.has(args.slot))
throw new Error(`unknown aopeng paper slot: ${String(args.slot)}`);
return {
slot: args.slot,
fields: parseSnapshotFields(args.fields)
};
},
validateResult: (value) => value === null || JsonRuleValueSchema.safeParse(value).success,
execute: ({ slot, fields }, { signal }) => {
let payload;
try {
payload = environment.readCapturedResponse(slot);
} catch (error) {
throw new RuleDomainError(
"aopeng_paper_unavailable",
error instanceof Error ? error.message : "paper data is unavailable"
);
}
if (payload == null || typeof payload !== "object") return null;
const state = createSnapshotState(signal, LABEL);
const result = /* @__PURE__ */ Object.create(null);
for (const [name, path] of Object.entries(fields)) {
const selected2 = pathValueThroughEmbeddedJson(payload, path);
result[name] = selected2 == null ? null : cloneJsonValue(selected2, state);
}
return result;
}
});
}
const DEFAULT_MAX_TREES = 1e3;
function normalizeHosts(hosts) {
const normalized = new Set(
hosts.map((host) => host.trim().toLowerCase()).filter(Boolean)
);
if (normalized.size === 0)
throw new Error("JsonRulePlatformAdapter requires at least one host");
for (const host of normalized) {
if (host.includes("/") || host.includes(":") || host.startsWith(".") || host.endsWith(".") || host.includes(".."))
throw new Error(`invalid JsonRulePlatformAdapter host: ${host}`);
}
return normalized;
}
function matchesHost(hostname, hosts) {
const current = hostname.toLowerCase();
for (const host of hosts) {
if (current === host || current.endsWith(`.${host}`)) return true;
}
return false;
}
function variantFlows(resolved) {
const { variant } = resolved;
return [
variant.match,
variant.capture,
variant.fill,
...variant.diagnostics ? [variant.diagnostics] : []
];
}
function publicFillPlan(plan) {
return Object.freeze({
path: plan.path,
atomic: plan.atomic,
fingerprint: plan.fingerprint,
operations: plan.operations
});
}
function isRuleFailure(error) {
return error instanceof RuleDomainError || error instanceof RuleExecutionError;
}
class JsonRulePlatformAdapter {
constructor(options) {
__publicField(this, "platform");
__publicField(this, "packageId");
__publicField(this, "hosts");
__publicField(this, "maxTrees");
__publicField(this, "treeRuntimes", /* @__PURE__ */ new WeakMap());
__publicField(this, "activeRuntime", null);
__publicField(this, "lastResolved", null);
__publicField(this, "captureFailure", null);
/**
* 最近一次变体匹配的逐条结果。**没有它,「为什么匹配到了这个变体」完全不可观测**
* ——2026-08-20 排查判分页收录不生效时,只能看到最终 variantId(还带
* `lastResolved` 回退,可能是上一页的残留),两个变体各自过没过、卡在哪一步,
* 屏幕上一个字都没有(坑 41:判据的输入要和判据的输出一样可观测)。
*/
__publicField(this, "lastAttempts", []);
__publicField(this, "captureGeneration", 0);
__publicField(this, "lastHarvested", []);
__publicField(this, "pageChangeListeners", /* @__PURE__ */ new Set());
this.options = options;
if (!options.platform.trim() || !options.packageId.trim())
throw new Error("JsonRulePlatformAdapter identifiers cannot be empty");
this.platform = options.platform;
this.packageId = options.packageId;
this.hosts = normalizeHosts(options.hosts);
this.maxTrees = options.maxTrees ?? DEFAULT_MAX_TREES;
new RuleCaptureRegistry({ maxTrees: this.maxTrees }).dispose();
}
/**
* 光域名对上不算命中:纯云端包在规则拉下来之前、或包被自动暂停时,store 里一条
* 都没有,capture 必然返回空。此时报「命中」会让诊断显示「命中<平台> · 抓到 0 题」,
* 把「没有规则」说成「规则没抓到题」,是两个完全不同的排查方向。
*/
match(ctx) {
return !ctx.signal.aborted && matchesHost(ctx.location.hostname, this.hosts) && this.options.store.resolve(this.packageId) !== null;
}
/** 判分页只收录、不产出可答树;收录结果与 bindings 无关,故单独取走。 */
takeHarvested() {
return this.lastHarvested.splice(0);
}
subscribePageChanges(listener) {
this.pageChangeListeners.add(listener);
let active2 = true;
return () => {
if (!active2) return;
active2 = false;
this.pageChangeListeners.delete(listener);
};
}
ruleDiagnostics() {
var _a2, _b, _c;
return {
resolved: this.options.store.resolve(this.packageId),
variantId: ((_b = (_a2 = this.activeRuntime) == null ? void 0 : _a2.resolved) == null ? void 0 : _b.variant.id) ?? ((_c = this.lastResolved) == null ? void 0 : _c.variant.id) ?? null,
attempts: this.lastAttempts,
captureFailure: this.captureFailure,
store: this.options.store.diagnostics(this.packageId)
};
}
async captureTrees(ctx) {
var _a2;
const generation = ++this.captureGeneration;
const previous = this.activeRuntime;
this.activeRuntime = null;
this.lastResolved = null;
this.captureFailure = null;
if (previous) await this.disposeRuntime(previous);
if (!this.match(ctx) || generation !== this.captureGeneration) return [];
const runtime = this.createRuntime(ctx);
try {
const resolved = await runtime.resolver.resolve(this.packageId, {
signal: ctx.signal
});
if (!resolved || resolved.pkg.platform !== this.platform) {
await this.disposeRuntime(runtime);
return [];
}
this.lastResolved = resolved;
runtime.resolved = resolved;
runtime.refs.tightenDomRefCap((_a2 = resolved.variant.limits) == null ? void 0 : _a2.maxDomRefs);
const result = await runtime.interpreter.run(resolved.variant.capture, {
phase: "capture",
signal: ctx.signal,
capabilities: new Set(resolved.pkg.capabilities),
limits: resolved.variant.limits,
flows: variantFlows(resolved),
resources: runtime.resources
});
this.lastHarvested.push(...runtime.capture.takeHarvested());
if (!runtime.capture.ownsFinishedResult(result.value))
throw new RuleDomainError(
"invalid_capture_result",
"capture flow must return capture.finish from its current registry"
);
if (generation !== this.captureGeneration) {
await this.disposeRuntime(runtime);
return [];
}
const trees = result.value;
if (trees.length === 0) {
if (!resolved.variant.lifecycle) {
await this.disposeRuntime(runtime);
return [];
}
this.activeRuntime = runtime;
await this.startLifecycle(runtime, resolved);
if (generation !== this.captureGeneration) {
if (this.activeRuntime === runtime) this.activeRuntime = null;
await this.disposeRuntime(runtime);
}
return [];
}
this.activeRuntime = runtime;
for (const tree of trees) this.treeRuntimes.set(tree, runtime);
return trees;
} catch (error) {
if (this.activeRuntime === runtime) this.activeRuntime = null;
await this.disposeRuntime(runtime);
if (isRuleFailure(error)) {
this.captureFailure = error.code;
return [];
}
throw error;
}
}
async applyTreeFillPlan(captured, plan, ctx) {
const runtime = this.treeRuntimes.get(captured);
if (!runtime || runtime.disposed || runtime !== this.activeRuntime || runtime.ctx.document !== ctx.document || runtime.ctx.location !== ctx.location || !this.match(ctx) || !runtime.resolved)
return false;
try {
assertSafetyCapability(plan.safetyCapability);
} catch {
return false;
}
if (safetyPlanForCapability(plan.safetyCapability) !== plan) return false;
try {
const result = await runtime.interpreter.run(
runtime.resolved.variant.fill,
{
phase: "fill",
signal: ctx.signal,
reserved: {
$safety: plan.safetyCapability,
$plan: publicFillPlan(plan)
},
capabilities: new Set(runtime.resolved.pkg.capabilities),
limits: runtime.resolved.variant.limits,
flows: variantFlows(runtime.resolved)
}
);
if (result.value !== true) return false;
return runtime.writer.verifyPlan(plan, ctx.signal);
} catch (error) {
if (isRuleFailure(error)) return false;
throw error;
}
}
async dispose() {
this.captureGeneration += 1;
const runtime = this.activeRuntime;
this.activeRuntime = null;
this.lastResolved = null;
this.pageChangeListeners.clear();
if (runtime) await this.disposeRuntime(runtime);
}
createRuntime(ctx) {
var _a2, _b;
const refs = new RuntimeReferenceRegistry({
maxDomRefs: this.options.policy.limits.maxDomRefs
});
const capture2 = new RuleCaptureRegistry({ maxTrees: this.maxTrees });
const resources = new RuleResourceScope();
resources.add(() => capture2.dispose());
resources.bind(ctx.signal);
const registry = new PrimitiveRegistry();
const writer = new BindingRegistryAnswerWriter(capture2.bindings);
const environment = {
ctx,
refs,
capture: capture2,
resources,
writer
};
const interpreter = new RuleInterpreter({
registry,
policy: this.options.policy,
services: this.options.services
});
this.lastAttempts = [];
const runtime = {
...environment,
interpreter,
resolver: new JsonRuleResolver({
store: this.options.store,
interpreter,
onAttempt: (attempt) => {
this.lastAttempts = [...this.lastAttempts, attempt];
}
}),
resolved: null,
lifecycle: null,
disposed: false
};
registerCoreRulePrimitives(registry, {
document: ctx.document,
location: ctx.location,
refs,
capture: capture2,
writer,
resources,
emit: (event) => {
void this.dispatchLifecycleEvent(runtime, event);
}
});
(_b = (_a2 = this.options).configureRegistry) == null ? void 0 : _b.call(_a2, registry, environment);
return runtime;
}
async startLifecycle(runtime, resolved) {
const definition = resolved.variant.lifecycle;
if (!definition) return;
const machine = new RuleStateMachine({
interpreter: runtime.interpreter,
definition,
flows: variantFlows(resolved),
capabilities: new Set(resolved.pkg.capabilities),
signal: runtime.ctx.signal,
resources: runtime.resources,
limits: resolved.variant.limits
});
runtime.lifecycle = machine;
await machine.start();
}
async dispatchLifecycleEvent(runtime, event) {
const lifecycle = runtime.lifecycle;
if (!lifecycle || runtime.disposed || runtime !== this.activeRuntime) return;
try {
const result = await lifecycle.dispatch(event.event, event.payload);
if (!result.transitioned || runtime.disposed || runtime !== this.activeRuntime)
return;
for (const listener of [...this.pageChangeListeners]) listener();
} catch {
}
}
async disposeRuntime(runtime) {
if (runtime.disposed) return;
runtime.disposed = true;
runtime.capture.dispose();
await runtime.resources.dispose();
}
}
const CHA0XING_FONT_TABLE_MD5 = "87594bb90a8153dd8fbe69683c451b1c";
function parseChaoxingFontTable(raw) {
if (!raw || cxFontMd5(raw) !== CHA0XING_FONT_TABLE_MD5) return null;
try {
const parsed = JSON.parse(raw);
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object")
return null;
const table = Object.fromEntries(
Object.entries(parsed).filter(([, value]) => typeof value === "number")
);
return Object.keys(table).length === 20902 ? table : null;
} catch {
return null;
}
}
const LOCAL_ANSWER_CACHE_KEY = "aiask_local_answers_v1";
const CACHE_WARN_ENTRIES = 5e3;
const HASH_PATTERN = /^[0-9a-f]{64}$/;
const MAX_VALUES = 64;
const MAX_OPTIONS = 64;
const HIT_PERSIST_INTERVAL_MS = 6e4;
function laterOf(a, b) {
return Math.max(a ?? 0, b ?? 0) || void 0;
}
const TOMBSTONE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
function parseOptions(input) {
if (!Array.isArray(input) || input.length === 0) return void 0;
if (input.length > MAX_OPTIONS) return void 0;
const options = [];
for (const option of input) {
if (typeof option !== "string" || !option.trim()) return void 0;
options.push(option);
}
return options;
}
function parseEntry(input) {
if (!input || typeof input !== "object") return null;
const raw = input;
const values = raw.values;
if (!Array.isArray(values) || values.length === 0 || values.length > MAX_VALUES)
return null;
const normalized = [];
for (const value of values) {
if (typeof value !== "string" || !value.trim()) return null;
normalized.push(value);
}
const text = (key) => typeof raw[key] === "string" && raw[key] ? String(raw[key]) : void 0;
const stamp = (key) => {
const at = raw[key];
return typeof at === "number" && at > 0 ? at : void 0;
};
return {
values: normalized,
// #67:题干完整存储不截断——搜索、导出迁移都要全文;界面截短交给显示层
stem: text("stem"),
itemType: text("itemType"),
platform: text("platform"),
options: parseOptions(raw.options),
savedAt: stamp("savedAt"),
// §10 未命中信号:本函数显式重建对象,**不列在这里的字段活不过一次读盘**。
// 存量条目没有这两个,取 undefined 即可——缺席就是「不是导入的 / 暂未命中」。
importedAt: stamp("importedAt"),
lastHitAt: stamp("lastHitAt")
};
}
function parseSnapshot$1(input) {
const entries = /* @__PURE__ */ new Map();
const tombstones = /* @__PURE__ */ new Map();
if (!input || typeof input !== "object")
return { entries, tombstones, clearedAt: 0 };
const snapshot2 = input;
if (Array.isArray(snapshot2.entries))
for (const item of snapshot2.entries) {
if (!Array.isArray(item) || item.length !== 2) continue;
const [key, value] = item;
if (typeof key !== "string" || !HASH_PATTERN.test(key)) continue;
const parsed = parseEntry(value);
if (parsed) entries.set(key, parsed);
}
if (Array.isArray(snapshot2.tombstones))
for (const item of snapshot2.tombstones) {
if (!Array.isArray(item) || item.length !== 2) continue;
const [key, at] = item;
if (typeof key !== "string" || !HASH_PATTERN.test(key)) continue;
if (typeof at !== "number" || !(at > 0)) continue;
tombstones.set(key, at);
}
const clearedAt = snapshot2.clearedAt;
return {
entries,
tombstones,
clearedAt: typeof clearedAt === "number" && clearedAt > 0 ? clearedAt : 0
};
}
class LocalAnswerCache {
constructor(storage) {
__publicField(this, "entries");
/** 当前平台标签:core 的 PlatformAdapter 不带 id,故由 userscript 在识别到平台时注入。 */
__publicField(this, "platform", "");
/**
* 删过的条目:unitHash → 删除时刻。合并时挡住不更新的旧副本,避免删了又复活。
* #75:连同 `clearedAt` 一起落盘(v4),否则删除意图只活在本标签页内存里——
* 另一个标签页拿着删除前的旧内存表落一次盘,就把删掉的条目原样写回来了。
*/
__publicField(this, "removedAt", /* @__PURE__ */ new Map());
/** 最近一次 clear() 的时刻:此前写入的条目一律不再留存,同样落盘。 */
__publicField(this, "clearedAt", 0);
/** #68:最近一次落盘是否失败。取消淘汰后表持续增长,写不进去必须让用户看见。 */
__publicField(this, "persistFailed", false);
/**
* #76:上一次落盘的哨兵——最新那条的 key 与 `savedAt`。
*
* `GM_setValue` 同步返回、异步落盘,配额耗尽通常**不会**同步抛回脚本,所以
* 「set 没抛」不等于「写成了」。只有下一次读盘才看得出来:哨兵还在盘上⇒上次成交,
* 不在⇒上次丢了。存一条哨兵而不是整份 key 集合,是因为配额撑爆时最先落不下去的
* 就是最新那条,而整份 key 集合要多占一份内存。
*/
__publicField(this, "lastWrite", null);
/** §10:上一次因命中而落盘的时刻,见 `HIT_PERSIST_INTERVAL_MS`。 */
__publicField(this, "lastHitPersistAt", 0);
/** §10:有命中被节流掉、只落在内存里,等 `flush()` 或下一次 persist 带上盘。 */
__publicField(this, "hitsPendingPersist", false);
this.storage = storage;
let loaded;
try {
loaded = parseSnapshot$1(this.storage.get(LOCAL_ANSWER_CACHE_KEY));
} catch {
loaded = { entries: /* @__PURE__ */ new Map(), tombstones: /* @__PURE__ */ new Map(), clearedAt: 0 };
}
this.entries = loaded.entries;
this.removedAt = loaded.tombstones;
this.clearedAt = loaded.clearedAt;
}
setPlatform(label) {
this.platform = label;
}
read(unitHash) {
const stored = this.entries.get(unitHash);
if (!stored) return null;
this.entries.delete(unitHash);
this.entries.set(unitHash, stored);
const now = Date.now();
stored.lastHitAt = now;
if (now - this.lastHitPersistAt > HIT_PERSIST_INTERVAL_MS) {
this.lastHitPersistAt = now;
this.persist();
} else this.hitsPendingPersist = true;
return {
values: [...stored.values],
...stored.itemType ? { itemType: stored.itemType } : {}
};
}
/**
* @returns 是否真的**入表**——非法 hash、非法值(含超过值数上限)返回 false,调用方据此留痕。
* 注意返回 true 不代表已落盘:落盘成败另看 `hasPersistFailure()`。
*/
write(unitHash, hit, meta) {
if (!HASH_PATTERN.test(unitHash)) return false;
const prev = this.entries.get(unitHash);
const parsed = parseEntry({
...hit,
stem: (meta == null ? void 0 : meta.stem) ?? (prev == null ? void 0 : prev.stem),
itemType: (meta == null ? void 0 : meta.itemType) ?? (prev == null ? void 0 : prev.itemType),
// this.platform 默认是空串(未调用过 setPlatform),空串不是 nullish,
// 直接 `?? this.platform ?? prev?.platform` 会在这里短路成 '',
// 让 prev?.platform 永远够不到——用 `|| prev?.platform` 把「空串当缺席」处理。
platform: (meta == null ? void 0 : meta.platform) ?? (this.platform || (prev == null ? void 0 : prev.platform)),
options: (meta == null ? void 0 : meta.options) ?? (prev == null ? void 0 : prev.options),
savedAt: Date.now(),
// §10:导入的题被做对一次就走这里收录。不带过来,「导入的 N 条」这个分母
// 会随用户答题凭空缩水,命中信号也会被重置成「暂未命中」。
importedAt: prev == null ? void 0 : prev.importedAt,
lastHitAt: prev == null ? void 0 : prev.lastHitAt
});
if (!parsed) return false;
this.entries.delete(unitHash);
this.entries.set(unitHash, parsed);
this.removedAt.delete(unitHash);
this.persist();
return true;
}
list() {
return [...this.entries].map(([unitHash, stored]) => ({
unitHash,
values: [...stored.values],
stem: stored.stem ?? "",
itemType: stored.itemType ?? "",
platform: stored.platform ?? "",
options: stored.options ? [...stored.options] : [],
savedAt: stored.savedAt ?? 0,
importedAt: stored.importedAt ?? 0,
lastHitAt: stored.lastHitAt ?? 0
})).reverse();
}
remove(unitHash) {
if (this.entries.delete(unitHash)) {
this.removedAt.set(unitHash, Date.now());
this.persist();
}
}
size() {
return this.entries.size;
}
/**
* §10:把被 `HIT_PERSIST_INTERVAL_MS` 节流掉、还只在内存里的命中落盘一次。
*
* 为什么非有不可:**新页面加载 = 新实例 = `lastHitPersistAt` 归 0**,于是第一次命中
* 落盘、其后一分钟内的命中全部只在内存里;而 `answer-session` 每题之间约 1.5 秒
* (`delayMs` 默认 1000 + 随机 1000)⇒ 一页 40 题以内的作业整场只有一两次落盘。
* 页面一关,其余命中连同它们的 `lastHitAt` 一起没了,下一次加载的缓存页把这些**已经
* 命中过**的题全报成「暂未命中」——§10 这个专为「别报假的」而存在的信号自己产假警报。
* 调用点在 `Panel.vue` 的 `pagehide`(挂载帧持有本单例,见 `gm.ts`)。
*
* 仍是尽力而为:`GM_setValue` 同步返回、异步落盘(见 `lastWrite`),卸载时这一次
* 能不能真上盘不由脚本决定 ⇒ 界面上「关页面太快就永远不计」那句照旧成立,别删。
*/
flush() {
if (this.hitsPendingPersist) this.persist();
}
/** #68:落盘失败后为 true,界面据此提示「存不下了」;下一次成功落盘自动复位。 */
hasPersistFailure() {
return this.persistFailed;
}
clear() {
this.entries.clear();
this.removedAt.clear();
this.clearedAt = Date.now();
this.persist();
}
exportJson() {
return JSON.stringify(this.snapshot(false), null, 2);
}
previewImport(text) {
const { incoming, rawCount } = this.parseImport(text);
let added = 0;
let replaced = 0;
for (const key of incoming.keys()) {
if (this.entries.has(key)) replaced += 1;
else added += 1;
}
return {
fileCount: incoming.size,
added,
replaced,
skipped: rawCount - incoming.size,
// #68:不再淘汰,导入后条数就是实际条数——原先的 Math.min 会把「静默删掉最旧的 N 条」
// 显示成「刚好到上限」,是本仓唯一一处预演数字与实际不符的地方。
total: this.entries.size + added
};
}
/**
* 解析导入文本一次,同时给出合法条目与原始条目数。
* 两者之差 = 被丢弃的非法条目 + 文件内重复 unitHash 被去重折叠的条目
* (正常导出件 key 唯一,只有手工拼接多份文件才会出现后者)。
*/
parseImport(text) {
var _a2;
const parsedRaw = JSON.parse(text);
return {
// #75:**只取 entries**。导入文件里的 tombstones / clearedAt 一律不认——
// 否则一份构造过的导入件就能删掉对方的缓存,而导入的承诺是「只增不删」。
incoming: parseSnapshot$1(parsedRaw).entries,
rawCount: ((_a2 = parsedRaw.entries) == null ? void 0 : _a2.length) ?? 0
};
}
importJson(text) {
const { incoming, rawCount } = this.parseImport(text);
let added = 0;
let replaced = 0;
for (const [key, value] of incoming) {
const prev = this.entries.get(key);
if (prev) {
replaced += 1;
this.entries.delete(key);
} else {
added += 1;
}
this.entries.set(key, {
...value,
importedAt: laterOf(value.importedAt, prev == null ? void 0 : prev.importedAt),
lastHitAt: laterOf(value.lastHitAt, prev == null ? void 0 : prev.lastHitAt)
});
this.removedAt.delete(key);
}
this.persist();
this.verifyLastPersist();
return {
added,
replaced,
skipped: rawCount - incoming.size,
total: this.entries.size
};
}
/**
* 回读一次盘,就地判定**刚刚那一次** `persist()` 有没有成交。
*
* 不再写一遍(整表重写 5000 条约 10.9ms,且会再引入一次同样滞后的判定)——
* 判定需要的只是「盘上现在有没有那个哨兵」,一次读就够。
*
* **只升不降**:判成 `lost` 才置位,`landed` 不清标志。清除由下一次 `persist()`
* 负责,这里少一个方向就少一条能把真失败洗掉的路。
*/
verifyLastPersist() {
if (!this.lastWrite) return;
try {
if (this.judgeLastWrite(
parseSnapshot$1(this.storage.get(LOCAL_ANSWER_CACHE_KEY))
) === "lost")
this.persistFailed = true;
} catch {
}
}
/**
* @param withTombstones 落盘要带墓碑(删除意图须跨标签页可见);
* 导出**不带**——导出件是「你的答案」的备份,不是删除日志,
* 何况导入侧一律忽略墓碑,带上去只会让人误以为导入能删对方的数据。
*/
snapshot(withTombstones) {
const entries = [...this.entries].map(
([key, value]) => [key, value]
);
if (!withTombstones) return { v: 4, entries };
const alive = Date.now() - TOMBSTONE_TTL_MS;
return {
v: 4,
entries,
tombstones: [...this.removedAt].filter(([, at]) => at > alive),
clearedAt: this.clearedAt
};
}
/**
* #71:GM 存储按脚本作用域跨标签页共享,而本实例只在构造时读过一次快照,
* 直接整表覆盖会把其他标签页在此期间写入的条目静默抹掉。
* 落盘前重读磁盘,把别处更新的条目按 savedAt 合并进内存表;
* 删除/清空各留时间戳,不更新的旧副本不得复活。
*
* #75:删除意图是**双向**的——磁盘上的墓碑同样要挡住我内存里的旧副本,
* 否则另一个标签页删掉的条目会被我这次落盘原样写回去。
*/
mergeFromDisk() {
let disk;
try {
disk = parseSnapshot$1(this.storage.get(LOCAL_ANSWER_CACHE_KEY));
} catch {
return "unknown";
}
const verdict = this.judgeLastWrite(disk);
this.clearedAt = Math.max(this.clearedAt, disk.clearedAt);
for (const [key, at] of disk.tombstones)
if (at > (this.removedAt.get(key) ?? 0)) this.removedAt.set(key, at);
for (const [key, mine] of this.entries) {
const savedAt = mine.savedAt ?? 0;
const removedAt = this.removedAt.get(key);
const shadowed = this.clearedAt > 0 && savedAt <= this.clearedAt || removedAt !== void 0 && savedAt <= removedAt;
if (shadowed) this.entries.delete(key);
}
for (const [key, value] of disk.entries) {
const savedAt = value.savedAt ?? 0;
if (savedAt <= this.clearedAt) continue;
const removedAt = this.removedAt.get(key);
if (removedAt !== void 0 && savedAt <= removedAt) continue;
const mine = this.entries.get(key);
const importedAt = laterOf(value.importedAt, mine == null ? void 0 : mine.importedAt);
const lastHitAt = laterOf(value.lastHitAt, mine == null ? void 0 : mine.lastHitAt);
if (mine && (mine.savedAt ?? 0) >= savedAt) {
mine.importedAt = importedAt;
mine.lastHitAt = lastHitAt;
continue;
}
this.entries.delete(key);
this.entries.set(key, { ...value, importedAt, lastHitAt });
this.removedAt.delete(key);
}
return verdict;
}
/**
* 判定上一次落盘的结果(#76)。只在**盘上确有证据**时才动标志:
* 哨兵还在(或被更新的值覆盖)⇒ 上次成交;哨兵不见了 ⇒ 上次没落盘。
*
* 别把「别的标签页删了它」误判成落盘失败:#75 之后墓碑与 `clearedAt` 都在盘上,
* 时间戳晚于哨兵就说明是删除意图把它拿掉的,那不是我们写失败。
*/
judgeLastWrite(disk) {
const sentinel = this.lastWrite;
if (!sentinel) return "unknown";
const onDisk = disk.entries.get(sentinel.key);
if (onDisk && (onDisk.savedAt ?? 0) >= sentinel.savedAt) return "landed";
const removedAt = disk.tombstones.get(sentinel.key) ?? 0;
if (removedAt >= sentinel.savedAt || disk.clearedAt >= sentinel.savedAt)
return "landed";
return "lost";
}
/** 取内存表里最新的一条当哨兵;表空则没有哨兵可留。 */
newestWrite() {
let best = null;
for (const [key, value] of this.entries) {
const savedAt = value.savedAt ?? 0;
if (savedAt > 0 && (!best || savedAt > best.savedAt))
best = { key, savedAt };
}
return best;
}
persist() {
this.hitsPendingPersist = false;
try {
const verdict = this.mergeFromDisk();
this.storage.set(LOCAL_ANSWER_CACHE_KEY, this.snapshot(true));
this.lastWrite = this.newestWrite();
this.persistFailed = verdict === "lost";
} catch {
this.persistFailed = true;
}
}
}
const PANEL_POSITION_KEY = "aiask_panel_position";
const PANEL_VIEWPORT_MARGIN = 16;
function parsePanelPositionSnapshot(input) {
if (input === null || typeof input !== "object" || Array.isArray(input)) {
return null;
}
const record = input;
if (record.schemaVersion !== 1) {
return null;
}
const { x, y } = record;
if (typeof x !== "number" || typeof y !== "number") {
return null;
}
if (!Number.isFinite(x) || !Number.isFinite(y)) {
return null;
}
return {
x: Math.round(x),
y: Math.round(y)
};
}
function createPanelPositionSnapshot(position2) {
return {
schemaVersion: 1,
x: Math.round(position2.x),
y: Math.round(position2.y)
};
}
function loadPanelPosition(storage) {
const raw = storage.get(PANEL_POSITION_KEY);
const parsed = parsePanelPositionSnapshot(raw);
if (parsed === null) {
storage.delete(PANEL_POSITION_KEY);
return null;
}
return parsed;
}
function savePanelPosition(storage, position2) {
storage.set(PANEL_POSITION_KEY, createPanelPositionSnapshot(position2));
}
function axisRange(free, margin) {
const max = free >= 2 * margin ? free - margin : free;
const min = free >= 2 * margin ? margin : 0;
return { min, max };
}
function clampAxis(value, free, margin) {
const { min, max } = axisRange(free, margin);
const rounded = Math.round(value);
if (rounded < min) return Math.round(min);
if (rounded > max) return Math.round(max);
return rounded;
}
function clampPanelPosition(position2, panel, viewport, margin = PANEL_VIEWPORT_MARGIN) {
const freeX = Math.max(0, viewport.width - panel.width);
const freeY = Math.max(0, viewport.height - panel.height);
return {
x: clampAxis(position2.x, freeX, margin),
y: clampAxis(position2.y, freeY, margin)
};
}
function remapAxis(value, fromFree, toFree, margin) {
const from = axisRange(fromFree, margin);
const to = axisRange(toFree, margin);
const clamped = clampAxis(value, fromFree, margin);
const ratio = from.max === from.min ? 0.5 : (clamped - from.min) / (from.max - from.min);
return Math.round(to.min + ratio * (to.max - to.min));
}
function remapPanelPosition(position2, fromPanel, toPanel, viewport, margin = PANEL_VIEWPORT_MARGIN) {
return {
x: remapAxis(
position2.x,
Math.max(0, viewport.width - fromPanel.width),
Math.max(0, viewport.width - toPanel.width),
margin
),
y: remapAxis(
position2.y,
Math.max(0, viewport.height - fromPanel.height),
Math.max(0, viewport.height - toPanel.height),
margin
)
};
}
function createBackendSecurityClient(options) {
const deviceKeys = new DeviceKeyManager(options.storage);
const sessions = new SecureSessionClient({
transport: options.transport,
baseUrl: options.baseUrl,
deviceKeys,
stateStorage: options.storage,
rootPublicJwks: [options.rootPublicJwk],
clientVersion: options.clientVersion,
requestedScope: options.requestedScope,
// GM 存储只有一份、后端地址却可被 dev 覆盖:遗留的全局 keyset 水位只算内置正式
// 后端那一份,否则本机 dev 会被上一次连生产学到的版本号永久判降级。
inheritLegacyKeysetWatermark: IS_DEFAULT_BACKEND
});
return {
sessions,
transport: new SecureTransport({
transport: options.transport,
sessions,
...options.requestedScope === "user" && options.getAccessToken ? { getAccessToken: options.getAccessToken } : {}
})
};
}
const CHA0XING_FONT_TABLE_RESOURCE = "chaoxingFontTable";
let chaoxingFontTable;
let chaoxingFontTableStatusValue;
const getChaoxingFontTable = () => {
if (chaoxingFontTable !== void 0) return chaoxingFontTable ?? {};
try {
const raw = _GM_getResourceText(CHA0XING_FONT_TABLE_RESOURCE);
chaoxingFontTable = parseChaoxingFontTable(raw);
chaoxingFontTableStatusValue = chaoxingFontTable ? "ok" : (
// 拿到了字符串却解析不出表 = 内容对不上 pin;空字符串仍算没拿到。
raw ? "rejected" : "unavailable"
);
} catch {
chaoxingFontTable = null;
chaoxingFontTableStatusValue = "unavailable";
}
return chaoxingFontTable ?? {};
};
const chaoxingFontTableStatus = () => {
getChaoxingFontTable();
return chaoxingFontTableStatusValue ?? "unavailable";
};
const gmTransport = {
send(req) {
return new Promise((resolve, reject) => {
_GM_xmlhttpRequest({
method: req.method,
url: req.url,
headers: req.headers,
data: req.body,
timeout: req.timeoutMs ?? 8e3,
// **今天挡的是零个 cookie**——后端从不下发 Set-Cookie(2026-08-23 公网
// 实测 /healthz、/captcha、/aiaskadmin/ 三处全为 0,Cloudflare 也没塞
// __cf_bm)。它是防御纵深:只要将来有任何一方开始给 www.aiask.site 种
// cookie(CF 开 bot management 就会),那些 cookie 会跟着**每一次搜题**
// 从课程页发出去。默认不带,比事后想起来再收干净。
anonymous: true,
onload: (r) => resolve({ status: r.status, body: r.responseText }),
ontimeout: () => reject(new Error("timeout")),
onerror: (e) => reject(new Error(`xhr error: ${(e == null ? void 0 : e.error) ?? "unknown"}`))
});
});
}
};
const TOKEN_KEY = "aiask_token";
const getToken = () => _GM_getValue(TOKEN_KEY, "") || "";
const setToken = (t) => _GM_setValue(TOKEN_KEY, t);
const clearToken = () => _GM_setValue(TOKEN_KEY, "");
const USERNAME_KEY = "aiask_username";
const getUsername = () => _GM_getValue(USERNAME_KEY, "") || "";
const setUsername = (u) => _GM_setValue(USERNAME_KEY, u);
const COLLAPSED_KEY = "aiask_panel_collapsed";
const getCollapsed = () => _GM_getValue(COLLAPSED_KEY, true);
const setCollapsed = (v) => _GM_setValue(COLLAPSED_KEY, v);
const panelPositionStorage = {
get: (key) => _GM_getValue(key, null),
set: (key, value) => _GM_setValue(key, value),
delete: (key) => _GM_deleteValue(key)
};
const getPanelPosition = () => loadPanelPosition(panelPositionStorage);
const setPanelPosition = (position2) => savePanelPosition(panelPositionStorage, position2);
const localAnswerCache = new LocalAnswerCache({
get: (key) => _GM_getValue(key, null),
set: (key, value) => _GM_setValue(key, value)
});
const CLIENT_ID_KEY = "aiask_client_id";
const getClientId = () => {
let id = _GM_getValue(CLIENT_ID_KEY, "") || "";
if (!id) {
id = crypto.randomUUID();
_GM_setValue(CLIENT_ID_KEY, id);
}
return id;
};
const SETTINGS_KEY = "aiask_settings";
const DEFAULT_SETTINGS = {
autoFill: true,
delayMs: 1500,
reportHealth: true,
freeFirst: true,
courseAuto: true,
coursePlaybackRate: 1,
courseTaskToggles: {
media: true,
"chapter-test": true,
reading: true,
hyperlink: true
},
autoStart: true,
autoSubmit: true,
autoSubmitThreshold: 0.8,
randomFallback: false
};
const getSettings = () => {
const raw = _GM_getValue(SETTINGS_KEY, null);
return raw ? {
...DEFAULT_SETTINGS,
...raw,
autoFill: true,
// 嵌套对象要单独并:老版本存下来的 blob 里没有新加的类型,
// 浅合并会让它们整体缺失,表现为「新类型默认关」——不是本意。
courseTaskToggles: {
...DEFAULT_SETTINGS.courseTaskToggles,
...raw.courseTaskToggles ?? {}
}
} : { ...DEFAULT_SETTINGS };
};
const setSettings = (s) => _GM_setValue(SETTINGS_KEY, s);
const BALANCE_KEY = "aiask_last_balance";
const getLastBalance = () => {
const raw = _GM_getValue(BALANCE_KEY, null);
return typeof raw === "number" && Number.isFinite(raw) ? raw : null;
};
const setLastBalance = (n) => _GM_setValue(BALANCE_KEY, n);
const clearLastBalance = () => _GM_setValue(BALANCE_KEY, null);
const gmSecurityStorage = {
get: (key) => _GM_getValue(key, void 0),
set: (key, value) => _GM_setValue(key, value),
delete: (key) => _GM_deleteValue(key)
};
const gmRuleStorage = {
get: (key) => _GM_getValue(key, void 0),
set: (key, value) => _GM_setValue(key, value),
delete: (key) => _GM_deleteValue(key)
};
const userSecurityClient = createBackendSecurityClient({
transport: gmTransport,
storage: gmSecurityStorage,
getAccessToken: getToken,
baseUrl: BACKEND_BASE_URL,
rootPublicJwk: SECURITY_ROOT_PUBLIC_JWK,
clientVersion: SCRIPT_VERSION,
requestedScope: "user"
});
const aiaskTransport = userSecurityClient.transport;
const ruleSecurityClient = createBackendSecurityClient({
transport: gmTransport,
storage: gmSecurityStorage,
baseUrl: BACKEND_BASE_URL,
rootPublicJwk: SECURITY_ROOT_PUBLIC_JWK,
clientVersion: SCRIPT_VERSION,
requestedScope: "report"
});
const ruleTransport = ruleSecurityClient.transport;
const IMPORT_BRIDGE_PATHNAME = "/import.html";
function isAllowedOrigin(origin) {
if (origin === IMPORT_BRIDGE_ORIGIN) return true;
return false;
}
function isImportBridgePage(url) {
return url.pathname === IMPORT_BRIDGE_PATHNAME && isAllowedOrigin(url.origin);
}
function errorReply(requestId2, reason) {
return {
channel: IMPORT_BRIDGE_REPLY_CHANNEL,
v: IMPORT_BRIDGE_VERSION,
requestId: requestId2,
kind: "error",
reason
};
}
function importBridgeReplyFor(event, cache, selfWindows) {
if (!isAllowedOrigin(event.origin)) return null;
if (!selfWindows.includes(event.source)) return null;
const request = parseImportBridgeRequest(event.data);
if (!request) return null;
if (request.kind === "ping")
return {
channel: IMPORT_BRIDGE_REPLY_CHANNEL,
v: IMPORT_BRIDGE_VERSION,
requestId: request.requestId,
kind: "pong",
scriptVersion: SCRIPT_VERSION
};
try {
if (request.kind === "preview")
return importBridgePreviewReply(
request.requestId,
cache.previewImport(request.snapshot)
);
const counts = cache.importJson(request.snapshot);
if (cache.hasPersistFailure())
return errorReply(request.requestId, "import-failed");
return importBridgeCommitReply(request.requestId, counts);
} catch {
return errorReply(request.requestId, "invalid-snapshot");
}
}
function looksLikeBridgeMessage(data) {
return typeof data === "object" && data !== null && data.channel === IMPORT_BRIDGE_CHANNEL;
}
function note(text, warn = false) {
const line = `[aiask] 导入桥接 · ${text}`;
if (warn) console.warn(line);
else console.info(line);
}
function installImportBridge(cache) {
const target = typeof unsafeWindow !== "undefined" && unsafeWindow || window;
const selfWindows = [target, window];
target.addEventListener("message", (event) => {
const reply = importBridgeReplyFor(event, cache, selfWindows);
if (!reply) {
if (looksLikeBridgeMessage(event.data))
note(
`未放行 · origin=${event.origin} source=${selfWindows.includes(event.source) ? "self" : "other"}`,
true
);
return;
}
target.postMessage(reply, event.origin);
note(`${reply.kind} → ${event.origin}`);
});
note(`已就绪 v${SCRIPT_VERSION} · ${location.origin}`);
}
const FRAME_READY_EVENT = "aiask:frame-ready";
function createPageChangeScheduler(view, callback, debounceMs = 100, maxWaitMs = 1e3) {
let timer = null;
let maxTimer = null;
let disposed = false;
const cancel = () => {
if (timer != null) view.clearTimeout(timer);
if (maxTimer != null) view.clearTimeout(maxTimer);
timer = null;
maxTimer = null;
};
const fire = () => {
cancel();
void callback();
};
return {
notify: () => {
if (disposed) return;
if (timer != null) view.clearTimeout(timer);
timer = view.setTimeout(fire, debounceMs);
if (maxTimer == null)
maxTimer = view.setTimeout(fire, Math.max(maxWaitMs, debounceMs));
},
cancel,
dispose: () => {
disposed = true;
cancel();
}
};
}
function subscribeDomChanges(document2, callback, options = {}) {
const view = document2.defaultView;
if (!view) throw new Error("dom-change document has no window");
const { debounceMs = 300, maxWaitMs = 1e3, maxTriggers: maxTriggers2 = 200 } = options;
let triggers = 0;
const scheduler = createPageChangeScheduler(
view,
() => {
triggers += 1;
if (triggers >= maxTriggers2) observer.disconnect();
return callback();
},
debounceMs,
maxWaitMs
);
const observer = new view.MutationObserver((records) => {
const host = document2.getElementById("aiask-host");
if (host && records.every((record) => host.contains(record.target))) return;
scheduler.notify();
});
observer.observe(document2.documentElement, {
attributes: true,
childList: true,
subtree: true
});
return () => {
observer.disconnect();
scheduler.dispose();
};
}
function subscribeUrlChanges(view, callback, debounceMs = 100) {
const history = view.history;
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
const scheduler = createPageChangeScheduler(view, callback, debounceMs);
const notify = () => scheduler.notify();
const wrappedPushState = function pushState(...args) {
originalPushState.apply(history, args);
notify();
};
const wrappedReplaceState = function replaceState(...args) {
originalReplaceState.apply(history, args);
notify();
};
history.pushState = wrappedPushState;
history.replaceState = wrappedReplaceState;
view.addEventListener("popstate", notify);
return () => {
view.removeEventListener("popstate", notify);
if (history.pushState === wrappedPushState)
history.pushState = originalPushState;
if (history.replaceState === wrappedReplaceState)
history.replaceState = originalReplaceState;
scheduler.dispose();
};
}
function notifyFrameReady(targetWindow) {
targetWindow.document.dispatchEvent(
new targetWindow.Event(FRAME_READY_EVENT)
);
}
function subscribeFrameReady(document2, callback, debounceMs = 100) {
const view = document2.defaultView;
if (!view) throw new Error("frame-ready document has no window");
let timer = null;
const listener = () => {
if (timer != null) view.clearTimeout(timer);
timer = view.setTimeout(() => {
timer = null;
void callback();
}, debounceMs);
};
document2.addEventListener(FRAME_READY_EVENT, listener);
return () => {
document2.removeEventListener(FRAME_READY_EVENT, listener);
if (timer != null) view.clearTimeout(timer);
timer = null;
};
}
function hasSupportedAncestor(ancestorOrigins, supportedHostPattern) {
for (const origin of ancestorOrigins) {
try {
if (supportedHostPattern.test(new URL(origin).hostname)) return true;
} catch {
}
}
return false;
}
function resolvePanelRole(input) {
if (input.isTop) return "mount";
if (!input.isHighestSameOrigin) return "relay-f9";
if (input.ancestorOrigins.length > 0) {
return hasSupportedAncestor(
input.ancestorOrigins,
input.supportedHostPattern
) ? "none" : "mount";
}
return "none";
}
function findHighestSameOriginWindow(start) {
let host = start;
try {
while (host.parent !== host && host.parent.location.href) host = host.parent;
} catch {
}
return host;
}
var Typr = {};
Typr.parse = function(buff) {
var bin = Typr._bin;
var data = new Uint8Array(buff);
var offset = 0;
bin.readFixed(data, offset);
offset += 4;
var numTables = bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
var tags = ["cmap", "head", "hhea", "maxp", "hmtx", "name", "OS/2", "post", "loca", "glyf", "kern", "CFF ", "GPOS", "GSUB", "SVG "];
var obj = { _data: data };
var tabs = {};
for (var i = 0; i < numTables; i++) {
var tag = bin.readASCII(data, offset, 4);
offset += 4;
bin.readUint(data, offset);
offset += 4;
var toffset = bin.readUint(data, offset);
offset += 4;
var length = bin.readUint(data, offset);
offset += 4;
tabs[tag] = { offset: toffset, length };
}
for (var i = 0; i < tags.length; i++) {
var t = tags[i];
if (tabs[t]) obj[t.trim()] = Typr[t.trim()].parse(data, tabs[t].offset, tabs[t].length, obj);
}
return obj;
};
Typr._tabOffset = function(data, tab) {
var bin = Typr._bin;
var numTables = bin.readUshort(data, 4);
var offset = 12;
for (var i = 0; i < numTables; i++) {
var tag = bin.readASCII(data, offset, 4);
offset += 4;
bin.readUint(data, offset);
offset += 4;
var toffset = bin.readUint(data, offset);
offset += 4;
bin.readUint(data, offset);
offset += 4;
if (tag == tab) return toffset;
}
return 0;
};
Typr._bin = { readFixed: function(data, o) {
return (data[o] << 8 | data[o + 1]) + (data[o + 2] << 8 | data[o + 3]) / (256 * 256 + 4);
}, readF2dot14: function(data, o) {
var num = Typr._bin.readShort(data, o);
return num / 16384;
}, readInt: function(buff, p) {
var a = Typr._bin.t.uint8;
a[0] = buff[p + 3];
a[1] = buff[p + 2];
a[2] = buff[p + 1];
a[3] = buff[p];
return Typr._bin.t.int32[0];
}, readInt8: function(buff, p) {
var a = Typr._bin.t.uint8;
a[0] = buff[p];
return Typr._bin.t.int8[0];
}, readShort: function(buff, p) {
var a = Typr._bin.t.uint8;
a[1] = buff[p];
a[0] = buff[p + 1];
return Typr._bin.t.int16[0];
}, readUshort: function(buff, p) {
return buff[p] << 8 | buff[p + 1];
}, readUshorts: function(buff, p, len) {
var arr = [];
for (var i = 0; i < len; i++) arr.push(Typr._bin.readUshort(buff, p + i * 2));
return arr;
}, readUint: function(buff, p) {
var a = Typr._bin.t.uint8;
a[3] = buff[p];
a[2] = buff[p + 1];
a[1] = buff[p + 2];
a[0] = buff[p + 3];
return Typr._bin.t.uint32[0];
}, readUint64: function(buff, p) {
return Typr._bin.readUint(buff, p) * (4294967295 + 1) + Typr._bin.readUint(buff, p + 4);
}, readASCII: function(buff, p, l) {
var s = "";
for (var i = 0; i < l; i++) s += String.fromCharCode(buff[p + i]);
return s;
}, readUnicode: function(buff, p, l) {
var s = "";
for (var i = 0; i < l; i++) {
var c = buff[p++] << 8 | buff[p++];
s += String.fromCharCode(c);
}
return s;
}, _tdec: window["TextDecoder"] ? new window["TextDecoder"]() : null, readUTF8: function(buff, p, l) {
var tdec = Typr._bin._tdec;
if (tdec && p == 0 && l == buff.length) return tdec["decode"](buff);
return Typr._bin.readASCII(buff, p, l);
}, readBytes: function(buff, p, l) {
var arr = [];
for (var i = 0; i < l; i++) arr.push(buff[p + i]);
return arr;
}, readASCIIArray: function(buff, p, l) {
var s = [];
for (var i = 0; i < l; i++) s.push(String.fromCharCode(buff[p + i]));
return s;
} };
Typr._bin.t = { buff: new ArrayBuffer(8) };
Typr._bin.t.int8 = new Int8Array(Typr._bin.t.buff);
Typr._bin.t.uint8 = new Uint8Array(Typr._bin.t.buff);
Typr._bin.t.int16 = new Int16Array(Typr._bin.t.buff);
Typr._bin.t.uint16 = new Uint16Array(Typr._bin.t.buff);
Typr._bin.t.int32 = new Int32Array(Typr._bin.t.buff);
Typr._bin.t.uint32 = new Uint32Array(Typr._bin.t.buff);
Typr._lctf = {};
Typr._lctf.parse = function(data, offset, length, font, subt) {
var bin = Typr._bin;
var obj = {};
var offset0 = offset;
bin.readFixed(data, offset);
offset += 4;
var offScriptList = bin.readUshort(data, offset);
offset += 2;
var offFeatureList = bin.readUshort(data, offset);
offset += 2;
var offLookupList = bin.readUshort(data, offset);
offset += 2;
obj.scriptList = Typr._lctf.readScriptList(data, offset0 + offScriptList);
obj.featureList = Typr._lctf.readFeatureList(data, offset0 + offFeatureList);
obj.lookupList = Typr._lctf.readLookupList(data, offset0 + offLookupList, subt);
return obj;
};
Typr._lctf.readLookupList = function(data, offset, subt) {
var bin = Typr._bin;
var offset0 = offset;
var obj = [];
var count2 = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < count2; i++) {
var noff = bin.readUshort(data, offset);
offset += 2;
var lut = Typr._lctf.readLookupTable(data, offset0 + noff, subt);
obj.push(lut);
}
return obj;
};
Typr._lctf.readLookupTable = function(data, offset, subt) {
var bin = Typr._bin;
var offset0 = offset;
var obj = { tabs: [] };
obj.ltype = bin.readUshort(data, offset);
offset += 2;
obj.flag = bin.readUshort(data, offset);
offset += 2;
var cnt = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < cnt; i++) {
var noff = bin.readUshort(data, offset);
offset += 2;
var tab = subt(data, obj.ltype, offset0 + noff);
obj.tabs.push(tab);
}
return obj;
};
Typr._lctf.numOfOnes = function(n) {
var num = 0;
for (var i = 0; i < 32; i++) if ((n >>> i & 1) != 0) num++;
return num;
};
Typr._lctf.readClassDef = function(data, offset) {
var bin = Typr._bin;
var obj = [];
var format = bin.readUshort(data, offset);
offset += 2;
if (format == 1) {
var startGlyph = bin.readUshort(data, offset);
offset += 2;
var glyphCount = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < glyphCount; i++) {
obj.push(startGlyph + i);
obj.push(startGlyph + i);
obj.push(bin.readUshort(data, offset));
offset += 2;
}
}
if (format == 2) {
var count2 = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < count2; i++) {
obj.push(bin.readUshort(data, offset));
offset += 2;
obj.push(bin.readUshort(data, offset));
offset += 2;
obj.push(bin.readUshort(data, offset));
offset += 2;
}
}
return obj;
};
Typr._lctf.getInterval = function(tab, val) {
for (var i = 0; i < tab.length; i += 3) {
var start = tab[i], end = tab[i + 1];
tab[i + 2];
if (start <= val && val <= end) return i;
}
return -1;
};
Typr._lctf.readValueRecord = function(data, offset, valFmt) {
var bin = Typr._bin;
var arr = [];
arr.push(valFmt & 1 ? bin.readShort(data, offset) : 0);
offset += valFmt & 1 ? 2 : 0;
arr.push(valFmt & 2 ? bin.readShort(data, offset) : 0);
offset += valFmt & 2 ? 2 : 0;
arr.push(valFmt & 4 ? bin.readShort(data, offset) : 0);
offset += valFmt & 4 ? 2 : 0;
arr.push(valFmt & 8 ? bin.readShort(data, offset) : 0);
offset += valFmt & 8 ? 2 : 0;
return arr;
};
Typr._lctf.readCoverage = function(data, offset) {
var bin = Typr._bin;
var cvg = {};
cvg.fmt = bin.readUshort(data, offset);
offset += 2;
var count2 = bin.readUshort(data, offset);
offset += 2;
if (cvg.fmt == 1) cvg.tab = bin.readUshorts(data, offset, count2);
if (cvg.fmt == 2) cvg.tab = bin.readUshorts(data, offset, count2 * 3);
return cvg;
};
Typr._lctf.coverageIndex = function(cvg, val) {
var tab = cvg.tab;
if (cvg.fmt == 1) return tab.indexOf(val);
if (cvg.fmt == 2) {
var ind = Typr._lctf.getInterval(tab, val);
if (ind != -1) return tab[ind + 2] + (val - tab[ind]);
}
return -1;
};
Typr._lctf.readFeatureList = function(data, offset) {
var bin = Typr._bin;
var offset0 = offset;
var obj = [];
var count2 = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < count2; i++) {
var tag = bin.readASCII(data, offset, 4);
offset += 4;
var noff = bin.readUshort(data, offset);
offset += 2;
obj.push({ tag: tag.trim(), tab: Typr._lctf.readFeatureTable(data, offset0 + noff) });
}
return obj;
};
Typr._lctf.readFeatureTable = function(data, offset) {
var bin = Typr._bin;
bin.readUshort(data, offset);
offset += 2;
var lookupCount = bin.readUshort(data, offset);
offset += 2;
var indices = [];
for (var i = 0; i < lookupCount; i++) indices.push(bin.readUshort(data, offset + 2 * i));
return indices;
};
Typr._lctf.readScriptList = function(data, offset) {
var bin = Typr._bin;
var offset0 = offset;
var obj = {};
var count2 = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < count2; i++) {
var tag = bin.readASCII(data, offset, 4);
offset += 4;
var noff = bin.readUshort(data, offset);
offset += 2;
obj[tag.trim()] = Typr._lctf.readScriptTable(data, offset0 + noff);
}
return obj;
};
Typr._lctf.readScriptTable = function(data, offset) {
var bin = Typr._bin;
var offset0 = offset;
var obj = {};
var defLangSysOff = bin.readUshort(data, offset);
offset += 2;
obj.default = Typr._lctf.readLangSysTable(data, offset0 + defLangSysOff);
var langSysCount = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < langSysCount; i++) {
var tag = bin.readASCII(data, offset, 4);
offset += 4;
var langSysOff = bin.readUshort(data, offset);
offset += 2;
obj[tag.trim()] = Typr._lctf.readLangSysTable(data, offset0 + langSysOff);
}
return obj;
};
Typr._lctf.readLangSysTable = function(data, offset) {
var bin = Typr._bin;
var obj = {};
bin.readUshort(data, offset);
offset += 2;
obj.reqFeature = bin.readUshort(data, offset);
offset += 2;
var featureCount = bin.readUshort(data, offset);
offset += 2;
obj.features = bin.readUshorts(data, offset, featureCount);
return obj;
};
Typr.CFF = {};
Typr.CFF.parse = function(data, offset, length) {
var bin = Typr._bin;
data = new Uint8Array(data.buffer, offset, length);
offset = 0;
data[offset];
offset++;
data[offset];
offset++;
data[offset];
offset++;
data[offset];
offset++;
var ninds = [];
offset = Typr.CFF.readIndex(data, offset, ninds);
var names = [];
for (var i = 0; i < ninds.length - 1; i++) names.push(bin.readASCII(data, offset + ninds[i], ninds[i + 1] - ninds[i]));
offset += ninds[ninds.length - 1];
var tdinds = [];
offset = Typr.CFF.readIndex(data, offset, tdinds);
var topDicts = [];
for (var i = 0; i < tdinds.length - 1; i++) topDicts.push(Typr.CFF.readDict(data, offset + tdinds[i], offset + tdinds[i + 1]));
offset += tdinds[tdinds.length - 1];
var topdict = topDicts[0];
var sinds = [];
offset = Typr.CFF.readIndex(data, offset, sinds);
var strings = [];
for (var i = 0; i < sinds.length - 1; i++) strings.push(bin.readASCII(data, offset + sinds[i], sinds[i + 1] - sinds[i]));
offset += sinds[sinds.length - 1];
Typr.CFF.readSubrs(data, offset, topdict);
if (topdict.CharStrings) {
offset = topdict.CharStrings;
var sinds = [];
offset = Typr.CFF.readIndex(data, offset, sinds);
var cstr = [];
for (var i = 0; i < sinds.length - 1; i++) cstr.push(bin.readBytes(data, offset + sinds[i], sinds[i + 1] - sinds[i]));
topdict.CharStrings = cstr;
}
if (topdict.Encoding) topdict.Encoding = Typr.CFF.readEncoding(data, topdict.Encoding, topdict.CharStrings.length);
if (topdict.charset) topdict.charset = Typr.CFF.readCharset(data, topdict.charset, topdict.CharStrings.length);
if (topdict.Private) {
offset = topdict.Private[1];
topdict.Private = Typr.CFF.readDict(data, offset, offset + topdict.Private[0]);
if (topdict.Private.Subrs) Typr.CFF.readSubrs(data, offset + topdict.Private.Subrs, topdict.Private);
}
var obj = {};
for (var p in topdict) {
if (["FamilyName", "FullName", "Notice", "version", "Copyright"].indexOf(p) != -1) obj[p] = strings[topdict[p] - 426 + 35];
else obj[p] = topdict[p];
}
return obj;
};
Typr.CFF.readSubrs = function(data, offset, obj) {
var bin = Typr._bin;
var gsubinds = [];
offset = Typr.CFF.readIndex(data, offset, gsubinds);
var bias, nSubrs = gsubinds.length;
if (nSubrs < 1240) bias = 107;
else if (nSubrs < 33900) bias = 1131;
else bias = 32768;
obj.Bias = bias;
obj.Subrs = [];
for (var i = 0; i < gsubinds.length - 1; i++) obj.Subrs.push(bin.readBytes(data, offset + gsubinds[i], gsubinds[i + 1] - gsubinds[i]));
};
Typr.CFF.tableSE = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 0, 111, 112, 113, 114, 0, 115, 116, 117, 118, 119, 120, 121, 122, 0, 123, 0, 124, 125, 126, 127, 128, 129, 130, 131, 0, 132, 133, 0, 134, 135, 136, 137, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 138, 0, 139, 0, 0, 0, 0, 140, 141, 142, 143, 0, 0, 0, 0, 0, 144, 0, 0, 0, 145, 0, 0, 146, 147, 148, 149, 0, 0, 0, 0];
Typr.CFF.glyphByUnicode = function(cff, code) {
for (var i = 0; i < cff.charset.length; i++) if (cff.charset[i] == code) return i;
return -1;
};
Typr.CFF.glyphBySE = function(cff, charcode) {
if (charcode < 0 || charcode > 255) return -1;
return Typr.CFF.glyphByUnicode(cff, Typr.CFF.tableSE[charcode]);
};
Typr.CFF.readEncoding = function(data, offset, num) {
Typr._bin;
var array = [".notdef"];
var format = data[offset];
offset++;
if (format == 0) {
var nCodes = data[offset];
offset++;
for (var i = 0; i < nCodes; i++) array.push(data[offset + i]);
} else throw "error: unknown encoding format: " + format;
return array;
};
Typr.CFF.readCharset = function(data, offset, num) {
var bin = Typr._bin;
var charset = [".notdef"];
var format = data[offset];
offset++;
if (format == 0) {
for (var i = 0; i < num; i++) {
var first = bin.readUshort(data, offset);
offset += 2;
charset.push(first);
}
} else if (format == 1 || format == 2) {
while (charset.length < num) {
var first = bin.readUshort(data, offset);
offset += 2;
var nLeft = 0;
if (format == 1) {
nLeft = data[offset];
offset++;
} else {
nLeft = bin.readUshort(data, offset);
offset += 2;
}
for (var i = 0; i <= nLeft; i++) {
charset.push(first);
first++;
}
}
} else throw "error: format: " + format;
return charset;
};
Typr.CFF.readIndex = function(data, offset, inds) {
var bin = Typr._bin;
var count2 = bin.readUshort(data, offset);
offset += 2;
var offsize = data[offset];
offset++;
if (offsize == 1) for (var i = 0; i < count2 + 1; i++) inds.push(data[offset + i]);
else if (offsize == 2) for (var i = 0; i < count2 + 1; i++) inds.push(bin.readUshort(data, offset + i * 2));
else if (offsize == 3) for (var i = 0; i < count2 + 1; i++) inds.push(bin.readUint(data, offset + i * 3 - 1) & 16777215);
else if (count2 != 0) throw "unsupported offset size: " + offsize + ", count: " + count2;
offset += (count2 + 1) * offsize;
return offset - 1;
};
Typr.CFF.getCharString = function(data, offset, o) {
var bin = Typr._bin;
var b0 = data[offset], b1 = data[offset + 1];
data[offset + 2];
data[offset + 3];
data[offset + 4];
var vs = 1;
var op = null, val = null;
if (b0 <= 20) {
op = b0;
vs = 1;
}
if (b0 == 12) {
op = b0 * 100 + b1;
vs = 2;
}
if (21 <= b0 && b0 <= 27) {
op = b0;
vs = 1;
}
if (b0 == 28) {
val = bin.readShort(data, offset + 1);
vs = 3;
}
if (29 <= b0 && b0 <= 31) {
op = b0;
vs = 1;
}
if (32 <= b0 && b0 <= 246) {
val = b0 - 139;
vs = 1;
}
if (247 <= b0 && b0 <= 250) {
val = (b0 - 247) * 256 + b1 + 108;
vs = 2;
}
if (251 <= b0 && b0 <= 254) {
val = -(b0 - 251) * 256 - b1 - 108;
vs = 2;
}
if (b0 == 255) {
val = bin.readInt(data, offset + 1) / 65535;
vs = 5;
}
o.val = val != null ? val : "o" + op;
o.size = vs;
};
Typr.CFF.readCharString = function(data, offset, length) {
var end = offset + length;
var bin = Typr._bin;
var arr = [];
while (offset < end) {
var b0 = data[offset], b1 = data[offset + 1];
data[offset + 2];
data[offset + 3];
data[offset + 4];
var vs = 1;
var op = null, val = null;
if (b0 <= 20) {
op = b0;
vs = 1;
}
if (b0 == 12) {
op = b0 * 100 + b1;
vs = 2;
}
if (b0 == 19 || b0 == 20) {
op = b0;
vs = 2;
}
if (21 <= b0 && b0 <= 27) {
op = b0;
vs = 1;
}
if (b0 == 28) {
val = bin.readShort(data, offset + 1);
vs = 3;
}
if (29 <= b0 && b0 <= 31) {
op = b0;
vs = 1;
}
if (32 <= b0 && b0 <= 246) {
val = b0 - 139;
vs = 1;
}
if (247 <= b0 && b0 <= 250) {
val = (b0 - 247) * 256 + b1 + 108;
vs = 2;
}
if (251 <= b0 && b0 <= 254) {
val = -(b0 - 251) * 256 - b1 - 108;
vs = 2;
}
if (b0 == 255) {
val = bin.readInt(data, offset + 1) / 65535;
vs = 5;
}
arr.push(val != null ? val : "o" + op);
offset += vs;
}
return arr;
};
Typr.CFF.readDict = function(data, offset, end) {
var bin = Typr._bin;
var dict = {};
var carr = [];
while (offset < end) {
var b0 = data[offset], b1 = data[offset + 1];
data[offset + 2];
data[offset + 3];
data[offset + 4];
var vs = 1;
var key = null, val = null;
if (b0 == 28) {
val = bin.readShort(data, offset + 1);
vs = 3;
}
if (b0 == 29) {
val = bin.readInt(data, offset + 1);
vs = 5;
}
if (32 <= b0 && b0 <= 246) {
val = b0 - 139;
vs = 1;
}
if (247 <= b0 && b0 <= 250) {
val = (b0 - 247) * 256 + b1 + 108;
vs = 2;
}
if (251 <= b0 && b0 <= 254) {
val = -(b0 - 251) * 256 - b1 - 108;
vs = 2;
}
if (b0 == 255) {
val = bin.readInt(data, offset + 1) / 65535;
vs = 5;
throw "unknown number";
}
if (b0 == 30) {
var nibs = [];
vs = 1;
while (true) {
var b = data[offset + vs];
vs++;
var nib0 = b >> 4, nib1 = b & 15;
if (nib0 != 15) nibs.push(nib0);
if (nib1 != 15) nibs.push(nib1);
if (nib1 == 15) break;
}
var s = "";
var chars = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ".", "e", "e-", "reserved", "-", "endOfNumber"];
for (var i = 0; i < nibs.length; i++) s += chars[nibs[i]];
val = parseFloat(s);
}
if (b0 <= 21) {
var keys = ["version", "Notice", "FullName", "FamilyName", "Weight", "FontBBox", "BlueValues", "OtherBlues", "FamilyBlues", "FamilyOtherBlues", "StdHW", "StdVW", "escape", "UniqueID", "XUID", "charset", "Encoding", "CharStrings", "Private", "Subrs", "defaultWidthX", "nominalWidthX"];
key = keys[b0];
vs = 1;
if (b0 == 12) {
var keys = ["Copyright", "isFixedPitch", "ItalicAngle", "UnderlinePosition", "UnderlineThickness", "PaintType", "CharstringType", "FontMatrix", "StrokeWidth", "BlueScale", "BlueShift", "BlueFuzz", "StemSnapH", "StemSnapV", "ForceBold", 0, 0, "LanguageGroup", "ExpansionFactor", "initialRandomSeed", "SyntheticBase", "PostScript", "BaseFontName", "BaseFontBlend", 0, 0, 0, 0, 0, 0, "ROS", "CIDFontVersion", "CIDFontRevision", "CIDFontType", "CIDCount", "UIDBase", "FDArray", "FDSelect", "FontName"];
key = keys[b1];
vs = 2;
}
}
if (key != null) {
dict[key] = carr.length == 1 ? carr[0] : carr;
carr = [];
} else carr.push(val);
offset += vs;
}
return dict;
};
Typr.cmap = {};
Typr.cmap.parse = function(data, offset, length) {
data = new Uint8Array(data.buffer, offset, length);
offset = 0;
var bin = Typr._bin;
var obj = {};
bin.readUshort(data, offset);
offset += 2;
var numTables = bin.readUshort(data, offset);
offset += 2;
var offs = [];
obj.tables = [];
for (var i = 0; i < numTables; i++) {
var platformID = bin.readUshort(data, offset);
offset += 2;
var encodingID = bin.readUshort(data, offset);
offset += 2;
var noffset = bin.readUint(data, offset);
offset += 4;
var id = "p" + platformID + "e" + encodingID;
var tind = offs.indexOf(noffset);
if (tind == -1) {
tind = obj.tables.length;
var subt;
offs.push(noffset);
var format = bin.readUshort(data, noffset);
if (format == 0) subt = Typr.cmap.parse0(data, noffset);
else if (format == 4) subt = Typr.cmap.parse4(data, noffset);
else if (format == 6) subt = Typr.cmap.parse6(data, noffset);
else if (format == 12) subt = Typr.cmap.parse12(data, noffset);
else console.log("unknown format: " + format, platformID, encodingID, noffset);
obj.tables.push(subt);
}
if (obj[id] != null) throw "multiple tables for one platform+encoding";
obj[id] = tind;
}
return obj;
};
Typr.cmap.parse0 = function(data, offset) {
var bin = Typr._bin;
var obj = {};
obj.format = bin.readUshort(data, offset);
offset += 2;
var len = bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
obj.map = [];
for (var i = 0; i < len - 6; i++) obj.map.push(data[offset + i]);
return obj;
};
Typr.cmap.parse4 = function(data, offset) {
var bin = Typr._bin;
var offset0 = offset;
var obj = {};
obj.format = bin.readUshort(data, offset);
offset += 2;
var length = bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
var segCountX2 = bin.readUshort(data, offset);
offset += 2;
var segCount = segCountX2 / 2;
obj.searchRange = bin.readUshort(data, offset);
offset += 2;
obj.entrySelector = bin.readUshort(data, offset);
offset += 2;
obj.rangeShift = bin.readUshort(data, offset);
offset += 2;
obj.endCount = bin.readUshorts(data, offset, segCount);
offset += segCount * 2;
offset += 2;
obj.startCount = bin.readUshorts(data, offset, segCount);
offset += segCount * 2;
obj.idDelta = [];
for (var i = 0; i < segCount; i++) {
obj.idDelta.push(bin.readShort(data, offset));
offset += 2;
}
obj.idRangeOffset = bin.readUshorts(data, offset, segCount);
offset += segCount * 2;
obj.glyphIdArray = [];
while (offset < offset0 + length) {
obj.glyphIdArray.push(bin.readUshort(data, offset));
offset += 2;
}
return obj;
};
Typr.cmap.parse6 = function(data, offset) {
var bin = Typr._bin;
var obj = {};
obj.format = bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
obj.firstCode = bin.readUshort(data, offset);
offset += 2;
var entryCount = bin.readUshort(data, offset);
offset += 2;
obj.glyphIdArray = [];
for (var i = 0; i < entryCount; i++) {
obj.glyphIdArray.push(bin.readUshort(data, offset));
offset += 2;
}
return obj;
};
Typr.cmap.parse12 = function(data, offset) {
var bin = Typr._bin;
var obj = {};
obj.format = bin.readUshort(data, offset);
offset += 2;
offset += 2;
bin.readUint(data, offset);
offset += 4;
bin.readUint(data, offset);
offset += 4;
var nGroups = bin.readUint(data, offset);
offset += 4;
obj.groups = [];
for (var i = 0; i < nGroups; i++) {
var off = offset + i * 12;
var startCharCode = bin.readUint(data, off + 0);
var endCharCode = bin.readUint(data, off + 4);
var startGlyphID = bin.readUint(data, off + 8);
obj.groups.push([startCharCode, endCharCode, startGlyphID]);
}
return obj;
};
Typr.glyf = {};
Typr.glyf.parse = function(data, offset, length, font) {
var obj = [];
for (var g = 0; g < font.maxp.numGlyphs; g++) obj.push(null);
return obj;
};
Typr.glyf._parseGlyf = function(font, g) {
var bin = Typr._bin;
var data = font._data;
var offset = Typr._tabOffset(data, "glyf") + font.loca[g];
if (font.loca[g] == font.loca[g + 1]) return null;
var gl = {};
gl.noc = bin.readShort(data, offset);
offset += 2;
gl.xMin = bin.readShort(data, offset);
offset += 2;
gl.yMin = bin.readShort(data, offset);
offset += 2;
gl.xMax = bin.readShort(data, offset);
offset += 2;
gl.yMax = bin.readShort(data, offset);
offset += 2;
if (gl.xMin >= gl.xMax || gl.yMin >= gl.yMax) return null;
if (gl.noc > 0) {
gl.endPts = [];
for (var i = 0; i < gl.noc; i++) {
gl.endPts.push(bin.readUshort(data, offset));
offset += 2;
}
var instructionLength = bin.readUshort(data, offset);
offset += 2;
if (data.length - offset < instructionLength) return null;
gl.instructions = bin.readBytes(data, offset, instructionLength);
offset += instructionLength;
var crdnum = gl.endPts[gl.noc - 1] + 1;
gl.flags = [];
for (var i = 0; i < crdnum; i++) {
var flag = data[offset];
offset++;
gl.flags.push(flag);
if ((flag & 8) != 0) {
var rep = data[offset];
offset++;
for (var j = 0; j < rep; j++) {
gl.flags.push(flag);
i++;
}
}
}
gl.xs = [];
for (var i = 0; i < crdnum; i++) {
var i8 = (gl.flags[i] & 2) != 0, same = (gl.flags[i] & 16) != 0;
if (i8) {
gl.xs.push(same ? data[offset] : -data[offset]);
offset++;
} else {
if (same) gl.xs.push(0);
else {
gl.xs.push(bin.readShort(data, offset));
offset += 2;
}
}
}
gl.ys = [];
for (var i = 0; i < crdnum; i++) {
var i8 = (gl.flags[i] & 4) != 0, same = (gl.flags[i] & 32) != 0;
if (i8) {
gl.ys.push(same ? data[offset] : -data[offset]);
offset++;
} else {
if (same) gl.ys.push(0);
else {
gl.ys.push(bin.readShort(data, offset));
offset += 2;
}
}
}
var x = 0, y = 0;
for (var i = 0; i < crdnum; i++) {
x += gl.xs[i];
y += gl.ys[i];
gl.xs[i] = x;
gl.ys[i] = y;
}
} else {
var ARG_1_AND_2_ARE_WORDS = 1 << 0;
var ARGS_ARE_XY_VALUES = 1 << 1;
var WE_HAVE_A_SCALE = 1 << 3;
var MORE_COMPONENTS = 1 << 5;
var WE_HAVE_AN_X_AND_Y_SCALE = 1 << 6;
var WE_HAVE_A_TWO_BY_TWO = 1 << 7;
var WE_HAVE_INSTRUCTIONS = 1 << 8;
gl.parts = [];
var flags;
do {
flags = bin.readUshort(data, offset);
offset += 2;
var part = { m: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 }, p1: -1, p2: -1 };
gl.parts.push(part);
part.glyphIndex = bin.readUshort(data, offset);
offset += 2;
if (flags & ARG_1_AND_2_ARE_WORDS) {
var arg1 = bin.readShort(data, offset);
offset += 2;
var arg2 = bin.readShort(data, offset);
offset += 2;
} else {
var arg1 = bin.readInt8(data, offset);
offset++;
var arg2 = bin.readInt8(data, offset);
offset++;
}
if (flags & ARGS_ARE_XY_VALUES) {
part.m.tx = arg1;
part.m.ty = arg2;
} else {
part.p1 = arg1;
part.p2 = arg2;
}
if (flags & WE_HAVE_A_SCALE) {
part.m.a = part.m.d = bin.readF2dot14(data, offset);
offset += 2;
} else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) {
part.m.a = bin.readF2dot14(data, offset);
offset += 2;
part.m.d = bin.readF2dot14(data, offset);
offset += 2;
} else if (flags & WE_HAVE_A_TWO_BY_TWO) {
part.m.a = bin.readF2dot14(data, offset);
offset += 2;
part.m.b = bin.readF2dot14(data, offset);
offset += 2;
part.m.c = bin.readF2dot14(data, offset);
offset += 2;
part.m.d = bin.readF2dot14(data, offset);
offset += 2;
}
} while (flags & MORE_COMPONENTS);
if (flags & WE_HAVE_INSTRUCTIONS) {
var numInstr = bin.readUshort(data, offset);
offset += 2;
gl.instr = [];
for (var i = 0; i < numInstr; i++) {
gl.instr.push(data[offset]);
offset++;
}
}
}
return gl;
};
Typr.GPOS = {};
Typr.GPOS.parse = function(data, offset, length, font) {
return Typr._lctf.parse(data, offset, length, font, Typr.GPOS.subt);
};
Typr.GPOS.subt = function(data, ltype, offset) {
if (ltype != 2) return null;
var bin = Typr._bin, offset0 = offset, tab = {};
tab.format = bin.readUshort(data, offset);
offset += 2;
var covOff = bin.readUshort(data, offset);
offset += 2;
tab.coverage = Typr._lctf.readCoverage(data, covOff + offset0);
tab.valFmt1 = bin.readUshort(data, offset);
offset += 2;
tab.valFmt2 = bin.readUshort(data, offset);
offset += 2;
var ones1 = Typr._lctf.numOfOnes(tab.valFmt1);
var ones2 = Typr._lctf.numOfOnes(tab.valFmt2);
if (tab.format == 1) {
tab.pairsets = [];
var count2 = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < count2; i++) {
var psoff = bin.readUshort(data, offset);
offset += 2;
psoff += offset0;
var pvcount = bin.readUshort(data, psoff);
psoff += 2;
var arr = [];
for (var j = 0; j < pvcount; j++) {
var gid2 = bin.readUshort(data, psoff);
psoff += 2;
var value1, value2;
if (tab.valFmt1 != 0) {
value1 = Typr._lctf.readValueRecord(data, psoff, tab.valFmt1);
psoff += ones1 * 2;
}
if (tab.valFmt2 != 0) {
value2 = Typr._lctf.readValueRecord(data, psoff, tab.valFmt2);
psoff += ones2 * 2;
}
arr.push({ gid2, val1: value1, val2: value2 });
}
tab.pairsets.push(arr);
}
}
if (tab.format == 2) {
var classDef1 = bin.readUshort(data, offset);
offset += 2;
var classDef2 = bin.readUshort(data, offset);
offset += 2;
var class1Count = bin.readUshort(data, offset);
offset += 2;
var class2Count = bin.readUshort(data, offset);
offset += 2;
tab.classDef1 = Typr._lctf.readClassDef(data, offset0 + classDef1);
tab.classDef2 = Typr._lctf.readClassDef(data, offset0 + classDef2);
tab.matrix = [];
for (var i = 0; i < class1Count; i++) {
var row = [];
for (var j = 0; j < class2Count; j++) {
var value1 = null, value2 = null;
if (tab.valFmt1 != 0) {
value1 = Typr._lctf.readValueRecord(data, offset, tab.valFmt1);
offset += ones1 * 2;
}
if (tab.valFmt2 != 0) {
value2 = Typr._lctf.readValueRecord(data, offset, tab.valFmt2);
offset += ones2 * 2;
}
row.push({ val1: value1, val2: value2 });
}
tab.matrix.push(row);
}
}
return tab;
};
Typr.GSUB = {};
Typr.GSUB.parse = function(data, offset, length, font) {
return Typr._lctf.parse(data, offset, length, font, Typr.GSUB.subt);
};
Typr.GSUB.subt = function(data, ltype, offset) {
var bin = Typr._bin, offset0 = offset, tab = {};
if (ltype != 1 && ltype != 4 && ltype != 5) return null;
tab.fmt = bin.readUshort(data, offset);
offset += 2;
var covOff = bin.readUshort(data, offset);
offset += 2;
tab.coverage = Typr._lctf.readCoverage(data, covOff + offset0);
if (ltype == 1) {
if (tab.fmt == 1) {
tab.delta = bin.readShort(data, offset);
offset += 2;
} else if (tab.fmt == 2) {
var cnt = bin.readUshort(data, offset);
offset += 2;
tab.newg = bin.readUshorts(data, offset, cnt);
offset += tab.newg.length * 2;
}
} else if (ltype == 4) {
tab.vals = [];
var cnt = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < cnt; i++) {
var loff = bin.readUshort(data, offset);
offset += 2;
tab.vals.push(Typr.GSUB.readLigatureSet(data, offset0 + loff));
}
} else if (ltype == 5) {
if (tab.fmt == 2) {
var cDefOffset = bin.readUshort(data, offset);
offset += 2;
tab.cDef = Typr._lctf.readClassDef(data, offset0 + cDefOffset);
tab.scset = [];
var subClassSetCount = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < subClassSetCount; i++) {
var scsOff = bin.readUshort(data, offset);
offset += 2;
tab.scset.push(scsOff == 0 ? null : Typr.GSUB.readSubClassSet(data, offset0 + scsOff));
}
} else console.log("unknown table format", tab.fmt);
}
return tab;
};
Typr.GSUB.readSubClassSet = function(data, offset) {
var rUs = Typr._bin.readUshort, offset0 = offset, lset = [];
var cnt = rUs(data, offset);
offset += 2;
for (var i = 0; i < cnt; i++) {
var loff = rUs(data, offset);
offset += 2;
lset.push(Typr.GSUB.readSubClassRule(data, offset0 + loff));
}
return lset;
};
Typr.GSUB.readSubClassRule = function(data, offset) {
var rUs = Typr._bin.readUshort, rule = {};
var gcount = rUs(data, offset);
offset += 2;
var scount = rUs(data, offset);
offset += 2;
rule.input = [];
for (var i = 0; i < gcount - 1; i++) {
rule.input.push(rUs(data, offset));
offset += 2;
}
rule.substLookupRecords = Typr.GSUB.readSubstLookupRecords(data, offset, scount);
return rule;
};
Typr.GSUB.readSubstLookupRecords = function(data, offset, cnt) {
var rUs = Typr._bin.readUshort;
var out = [];
for (var i = 0; i < cnt; i++) {
out.push(rUs(data, offset), rUs(data, offset + 2));
offset += 4;
}
return out;
};
Typr.GSUB.readChainSubClassSet = function(data, offset) {
var bin = Typr._bin, offset0 = offset, lset = [];
var cnt = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < cnt; i++) {
var loff = bin.readUshort(data, offset);
offset += 2;
lset.push(Typr.GSUB.readChainSubClassRule(data, offset0 + loff));
}
return lset;
};
Typr.GSUB.readChainSubClassRule = function(data, offset) {
var bin = Typr._bin, rule = {};
var pps = ["backtrack", "input", "lookahead"];
for (var pi = 0; pi < pps.length; pi++) {
var cnt = bin.readUshort(data, offset);
offset += 2;
if (pi == 1) cnt--;
rule[pps[pi]] = bin.readUshorts(data, offset, cnt);
offset += rule[pps[pi]].length * 2;
}
var cnt = bin.readUshort(data, offset);
offset += 2;
rule.subst = bin.readUshorts(data, offset, cnt * 2);
offset += rule.subst.length * 2;
return rule;
};
Typr.GSUB.readLigatureSet = function(data, offset) {
var bin = Typr._bin, offset0 = offset, lset = [];
var lcnt = bin.readUshort(data, offset);
offset += 2;
for (var j = 0; j < lcnt; j++) {
var loff = bin.readUshort(data, offset);
offset += 2;
lset.push(Typr.GSUB.readLigature(data, offset0 + loff));
}
return lset;
};
Typr.GSUB.readLigature = function(data, offset) {
var bin = Typr._bin, lig = { chain: [] };
lig.nglyph = bin.readUshort(data, offset);
offset += 2;
var ccnt = bin.readUshort(data, offset);
offset += 2;
for (var k = 0; k < ccnt - 1; k++) {
lig.chain.push(bin.readUshort(data, offset));
offset += 2;
}
return lig;
};
Typr.head = {};
Typr.head.parse = function(data, offset, length) {
var bin = Typr._bin;
var obj = {};
bin.readFixed(data, offset);
offset += 4;
obj.fontRevision = bin.readFixed(data, offset);
offset += 4;
bin.readUint(data, offset);
offset += 4;
bin.readUint(data, offset);
offset += 4;
obj.flags = bin.readUshort(data, offset);
offset += 2;
obj.unitsPerEm = bin.readUshort(data, offset);
offset += 2;
obj.created = bin.readUint64(data, offset);
offset += 8;
obj.modified = bin.readUint64(data, offset);
offset += 8;
obj.xMin = bin.readShort(data, offset);
offset += 2;
obj.yMin = bin.readShort(data, offset);
offset += 2;
obj.xMax = bin.readShort(data, offset);
offset += 2;
obj.yMax = bin.readShort(data, offset);
offset += 2;
obj.macStyle = bin.readUshort(data, offset);
offset += 2;
obj.lowestRecPPEM = bin.readUshort(data, offset);
offset += 2;
obj.fontDirectionHint = bin.readShort(data, offset);
offset += 2;
obj.indexToLocFormat = bin.readShort(data, offset);
offset += 2;
obj.glyphDataFormat = bin.readShort(data, offset);
offset += 2;
return obj;
};
Typr.hhea = {};
Typr.hhea.parse = function(data, offset, length) {
var bin = Typr._bin;
var obj = {};
bin.readFixed(data, offset);
offset += 4;
obj.ascender = bin.readShort(data, offset);
offset += 2;
obj.descender = bin.readShort(data, offset);
offset += 2;
obj.lineGap = bin.readShort(data, offset);
offset += 2;
obj.advanceWidthMax = bin.readUshort(data, offset);
offset += 2;
obj.minLeftSideBearing = bin.readShort(data, offset);
offset += 2;
obj.minRightSideBearing = bin.readShort(data, offset);
offset += 2;
obj.xMaxExtent = bin.readShort(data, offset);
offset += 2;
obj.caretSlopeRise = bin.readShort(data, offset);
offset += 2;
obj.caretSlopeRun = bin.readShort(data, offset);
offset += 2;
obj.caretOffset = bin.readShort(data, offset);
offset += 2;
offset += 4 * 2;
obj.metricDataFormat = bin.readShort(data, offset);
offset += 2;
obj.numberOfHMetrics = bin.readUshort(data, offset);
offset += 2;
return obj;
};
Typr.hmtx = {};
Typr.hmtx.parse = function(data, offset, length, font) {
var bin = Typr._bin;
var obj = {};
obj.aWidth = [];
obj.lsBearing = [];
var aw = 0, lsb = 0;
for (var i = 0; i < font.maxp.numGlyphs; i++) {
if (i < font.hhea.numberOfHMetrics) {
aw = bin.readUshort(data, offset);
offset += 2;
lsb = bin.readShort(data, offset);
offset += 2;
}
obj.aWidth.push(aw);
obj.lsBearing.push(lsb);
}
return obj;
};
Typr.kern = {};
Typr.kern.parse = function(data, offset, length, font) {
var bin = Typr._bin;
var version = bin.readUshort(data, offset);
offset += 2;
if (version == 1) return Typr.kern.parseV1(data, offset - 2, length, font);
var nTables = bin.readUshort(data, offset);
offset += 2;
var map = { glyph1: [], rval: [] };
for (var i = 0; i < nTables; i++) {
offset += 2;
var length = bin.readUshort(data, offset);
offset += 2;
var coverage = bin.readUshort(data, offset);
offset += 2;
var format = coverage >>> 8;
format &= 15;
if (format == 0) offset = Typr.kern.readFormat0(data, offset, map);
else throw "unknown kern table format: " + format;
}
return map;
};
Typr.kern.parseV1 = function(data, offset, length, font) {
var bin = Typr._bin;
bin.readFixed(data, offset);
offset += 4;
var nTables = bin.readUint(data, offset);
offset += 4;
var map = { glyph1: [], rval: [] };
for (var i = 0; i < nTables; i++) {
bin.readUint(data, offset);
offset += 4;
var coverage = bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
var format = coverage >>> 8;
format &= 15;
if (format == 0) offset = Typr.kern.readFormat0(data, offset, map);
else throw "unknown kern table format: " + format;
}
return map;
};
Typr.kern.readFormat0 = function(data, offset, map) {
var bin = Typr._bin;
var pleft = -1;
var nPairs = bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
for (var j = 0; j < nPairs; j++) {
var left = bin.readUshort(data, offset);
offset += 2;
var right = bin.readUshort(data, offset);
offset += 2;
var value = bin.readShort(data, offset);
offset += 2;
if (left != pleft) {
map.glyph1.push(left);
map.rval.push({ glyph2: [], vals: [] });
}
var rval = map.rval[map.rval.length - 1];
rval.glyph2.push(right);
rval.vals.push(value);
pleft = left;
}
return offset;
};
Typr.loca = {};
Typr.loca.parse = function(data, offset, length, font) {
var bin = Typr._bin;
var obj = [];
var ver = font.head.indexToLocFormat;
var len = font.maxp.numGlyphs + 1;
if (ver == 0) for (var i = 0; i < len; i++) obj.push(bin.readUshort(data, offset + (i << 1)) << 1);
if (ver == 1) for (var i = 0; i < len; i++) obj.push(bin.readUint(data, offset + (i << 2)));
return obj;
};
Typr.maxp = {};
Typr.maxp.parse = function(data, offset, length) {
var bin = Typr._bin;
var obj = {};
var ver = bin.readUint(data, offset);
offset += 4;
obj.numGlyphs = bin.readUshort(data, offset);
offset += 2;
if (ver == 65536) {
obj.maxPoints = bin.readUshort(data, offset);
offset += 2;
obj.maxContours = bin.readUshort(data, offset);
offset += 2;
obj.maxCompositePoints = bin.readUshort(data, offset);
offset += 2;
obj.maxCompositeContours = bin.readUshort(data, offset);
offset += 2;
obj.maxZones = bin.readUshort(data, offset);
offset += 2;
obj.maxTwilightPoints = bin.readUshort(data, offset);
offset += 2;
obj.maxStorage = bin.readUshort(data, offset);
offset += 2;
obj.maxFunctionDefs = bin.readUshort(data, offset);
offset += 2;
obj.maxInstructionDefs = bin.readUshort(data, offset);
offset += 2;
obj.maxStackElements = bin.readUshort(data, offset);
offset += 2;
obj.maxSizeOfInstructions = bin.readUshort(data, offset);
offset += 2;
obj.maxComponentElements = bin.readUshort(data, offset);
offset += 2;
obj.maxComponentDepth = bin.readUshort(data, offset);
offset += 2;
}
return obj;
};
Typr.name = {};
Typr.name.parse = function(data, offset, length) {
var bin = Typr._bin;
var obj = {};
bin.readUshort(data, offset);
offset += 2;
var count2 = bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
var offset0 = offset;
for (var i = 0; i < count2; i++) {
var platformID = bin.readUshort(data, offset);
offset += 2;
var encodingID = bin.readUshort(data, offset);
offset += 2;
var languageID = bin.readUshort(data, offset);
offset += 2;
var nameID = bin.readUshort(data, offset);
offset += 2;
var length = bin.readUshort(data, offset);
offset += 2;
var noffset = bin.readUshort(data, offset);
offset += 2;
var plat = "p" + platformID;
if (obj[plat] == null) obj[plat] = {};
var names = ["copyright", "fontFamily", "fontSubfamily", "ID", "fullName", "version", "postScriptName", "trademark", "manufacturer", "designer", "description", "urlVendor", "urlDesigner", "licence", "licenceURL", "---", "typoFamilyName", "typoSubfamilyName", "compatibleFull", "sampleText", "postScriptCID", "wwsFamilyName", "wwsSubfamilyName", "lightPalette", "darkPalette"];
var cname = names[nameID];
var soff = offset0 + count2 * 12 + noffset;
var str;
if (platformID == 0) str = bin.readUnicode(data, soff, length / 2);
else if (platformID == 3 && encodingID == 0) str = bin.readUnicode(data, soff, length / 2);
else if (encodingID == 0) str = bin.readASCII(data, soff, length);
else if (encodingID == 1) str = bin.readUnicode(data, soff, length / 2);
else if (encodingID == 3) str = bin.readUnicode(data, soff, length / 2);
else if (platformID == 1) {
str = bin.readASCII(data, soff, length);
console.log("reading unknown MAC encoding " + encodingID + " as ASCII");
} else throw "unknown encoding " + encodingID + ", platformID: " + platformID;
obj[plat][cname] = str;
obj[plat]._lang = languageID;
}
for (var p in obj) if (obj[p].postScriptName != null && obj[p]._lang == 1033) return obj[p];
for (var p in obj) if (obj[p].postScriptName != null && obj[p]._lang == 3084) return obj[p];
for (var p in obj) if (obj[p].postScriptName != null) return obj[p];
var tname;
for (var p in obj) {
tname = p;
break;
}
console.log("returning name table with languageID " + obj[tname]._lang);
return obj[tname];
};
Typr["OS/2"] = {};
Typr["OS/2"].parse = function(data, offset, length) {
var bin = Typr._bin;
var ver = bin.readUshort(data, offset);
offset += 2;
var obj = {};
if (ver == 0) Typr["OS/2"].version0(data, offset, obj);
else if (ver == 1) Typr["OS/2"].version1(data, offset, obj);
else if (ver == 2 || ver == 3 || ver == 4) Typr["OS/2"].version2(data, offset, obj);
else if (ver == 5) Typr["OS/2"].version5(data, offset, obj);
else throw "unknown OS/2 table version: " + ver;
return obj;
};
Typr["OS/2"].version0 = function(data, offset, obj) {
var bin = Typr._bin;
obj.xAvgCharWidth = bin.readShort(data, offset);
offset += 2;
obj.usWeightClass = bin.readUshort(data, offset);
offset += 2;
obj.usWidthClass = bin.readUshort(data, offset);
offset += 2;
obj.fsType = bin.readUshort(data, offset);
offset += 2;
obj.ySubscriptXSize = bin.readShort(data, offset);
offset += 2;
obj.ySubscriptYSize = bin.readShort(data, offset);
offset += 2;
obj.ySubscriptXOffset = bin.readShort(data, offset);
offset += 2;
obj.ySubscriptYOffset = bin.readShort(data, offset);
offset += 2;
obj.ySuperscriptXSize = bin.readShort(data, offset);
offset += 2;
obj.ySuperscriptYSize = bin.readShort(data, offset);
offset += 2;
obj.ySuperscriptXOffset = bin.readShort(data, offset);
offset += 2;
obj.ySuperscriptYOffset = bin.readShort(data, offset);
offset += 2;
obj.yStrikeoutSize = bin.readShort(data, offset);
offset += 2;
obj.yStrikeoutPosition = bin.readShort(data, offset);
offset += 2;
obj.sFamilyClass = bin.readShort(data, offset);
offset += 2;
obj.panose = bin.readBytes(data, offset, 10);
offset += 10;
obj.ulUnicodeRange1 = bin.readUint(data, offset);
offset += 4;
obj.ulUnicodeRange2 = bin.readUint(data, offset);
offset += 4;
obj.ulUnicodeRange3 = bin.readUint(data, offset);
offset += 4;
obj.ulUnicodeRange4 = bin.readUint(data, offset);
offset += 4;
obj.achVendID = [bin.readInt8(data, offset), bin.readInt8(data, offset + 1), bin.readInt8(data, offset + 2), bin.readInt8(data, offset + 3)];
offset += 4;
obj.fsSelection = bin.readUshort(data, offset);
offset += 2;
obj.usFirstCharIndex = bin.readUshort(data, offset);
offset += 2;
obj.usLastCharIndex = bin.readUshort(data, offset);
offset += 2;
obj.sTypoAscender = bin.readShort(data, offset);
offset += 2;
obj.sTypoDescender = bin.readShort(data, offset);
offset += 2;
obj.sTypoLineGap = bin.readShort(data, offset);
offset += 2;
obj.usWinAscent = bin.readUshort(data, offset);
offset += 2;
obj.usWinDescent = bin.readUshort(data, offset);
offset += 2;
return offset;
};
Typr["OS/2"].version1 = function(data, offset, obj) {
var bin = Typr._bin;
offset = Typr["OS/2"].version0(data, offset, obj);
obj.ulCodePageRange1 = bin.readUint(data, offset);
offset += 4;
obj.ulCodePageRange2 = bin.readUint(data, offset);
offset += 4;
return offset;
};
Typr["OS/2"].version2 = function(data, offset, obj) {
var bin = Typr._bin;
offset = Typr["OS/2"].version1(data, offset, obj);
obj.sxHeight = bin.readShort(data, offset);
offset += 2;
obj.sCapHeight = bin.readShort(data, offset);
offset += 2;
obj.usDefault = bin.readUshort(data, offset);
offset += 2;
obj.usBreak = bin.readUshort(data, offset);
offset += 2;
obj.usMaxContext = bin.readUshort(data, offset);
offset += 2;
return offset;
};
Typr["OS/2"].version5 = function(data, offset, obj) {
var bin = Typr._bin;
offset = Typr["OS/2"].version2(data, offset, obj);
obj.usLowerOpticalPointSize = bin.readUshort(data, offset);
offset += 2;
obj.usUpperOpticalPointSize = bin.readUshort(data, offset);
offset += 2;
return offset;
};
Typr.post = {};
Typr.post.parse = function(data, offset, length) {
var bin = Typr._bin;
var obj = {};
obj.version = bin.readFixed(data, offset);
offset += 4;
obj.italicAngle = bin.readFixed(data, offset);
offset += 4;
obj.underlinePosition = bin.readShort(data, offset);
offset += 2;
obj.underlineThickness = bin.readShort(data, offset);
offset += 2;
return obj;
};
Typr.SVG = {};
Typr.SVG.parse = function(data, offset, length) {
var bin = Typr._bin;
var obj = { entries: [] };
var offset0 = offset;
bin.readUshort(data, offset);
offset += 2;
var svgDocIndexOffset = bin.readUint(data, offset);
offset += 4;
bin.readUint(data, offset);
offset += 4;
offset = svgDocIndexOffset + offset0;
var numEntries = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < numEntries; i++) {
var startGlyphID = bin.readUshort(data, offset);
offset += 2;
var endGlyphID = bin.readUshort(data, offset);
offset += 2;
var svgDocOffset = bin.readUint(data, offset);
offset += 4;
var svgDocLength = bin.readUint(data, offset);
offset += 4;
var sbuf = new Uint8Array(data.buffer, offset0 + svgDocOffset + svgDocIndexOffset, svgDocLength);
var svg = bin.readUTF8(sbuf, 0, sbuf.length);
for (var f = startGlyphID; f <= endGlyphID; f++) {
obj.entries[f] = svg;
}
}
return obj;
};
Typr.SVG.toPath = function(str) {
var pth = { cmds: [], crds: [] };
if (str == null) return pth;
var prsr = new DOMParser();
var doc = prsr["parseFromString"](str, "image/svg+xml");
var svg = doc.firstChild;
while (svg.tagName != "svg") svg = svg.nextSibling;
var vb = svg.getAttribute("viewBox");
if (vb) vb = vb.trim().split(" ").map(parseFloat);
else vb = [0, 0, 1e3, 1e3];
Typr.SVG._toPath(svg.children, pth);
for (var i = 0; i < pth.crds.length; i += 2) {
var x = pth.crds[i], y = pth.crds[i + 1];
x -= vb[0];
y -= vb[1];
y = -y;
pth.crds[i] = x;
pth.crds[i + 1] = y;
}
return pth;
};
Typr.SVG._toPath = function(nds, pth, fill) {
for (var ni = 0; ni < nds.length; ni++) {
var nd = nds[ni], tn = nd.tagName;
var cfl = nd.getAttribute("fill");
if (cfl == null) cfl = fill;
if (tn == "g") Typr.SVG._toPath(nd.children, pth, cfl);
else if (tn == "path") {
pth.cmds.push(cfl ? cfl : "#000000");
var d = nd.getAttribute("d");
var toks = Typr.SVG._tokens(d);
Typr.SVG._toksToPath(toks, pth);
pth.cmds.push("X");
} else if (tn == "defs") ;
else console.log(tn, nd);
}
};
Typr.SVG._tokens = function(d) {
var ts = [], off = 0, rn = false, cn = "";
while (off < d.length) {
var cc = d.charCodeAt(off), ch = d.charAt(off);
off++;
var isNum = 48 <= cc && cc <= 57 || ch == "." || ch == "-";
if (rn) {
if (ch == "-") {
ts.push(parseFloat(cn));
cn = ch;
} else if (isNum) cn += ch;
else {
ts.push(parseFloat(cn));
if (ch != "," && ch != " ") ts.push(ch);
rn = false;
}
} else {
if (isNum) {
cn = ch;
rn = true;
} else if (ch != "," && ch != " ") ts.push(ch);
}
}
if (rn) ts.push(parseFloat(cn));
return ts;
};
Typr.SVG._toksToPath = function(ts, pth) {
var i = 0, x = 0, y = 0, ox = 0, oy = 0;
var pc = { M: 2, L: 2, H: 1, V: 1, S: 4, C: 6 };
var cmds = pth.cmds, crds = pth.crds;
while (i < ts.length) {
var cmd = ts[i];
i++;
if (cmd == "z") {
cmds.push("Z");
x = ox;
y = oy;
} else {
var cmu = cmd.toUpperCase();
var ps = pc[cmu], reps = Typr.SVG._reps(ts, i, ps);
for (var j = 0; j < reps; j++) {
var xi = 0, yi = 0;
if (cmd != cmu) {
xi = x;
yi = y;
}
if (cmu == "M") {
x = xi + ts[i++];
y = yi + ts[i++];
cmds.push("M");
crds.push(x, y);
ox = x;
oy = y;
} else if (cmu == "L") {
x = xi + ts[i++];
y = yi + ts[i++];
cmds.push("L");
crds.push(x, y);
} else if (cmu == "H") {
x = xi + ts[i++];
cmds.push("L");
crds.push(x, y);
} else if (cmu == "V") {
y = yi + ts[i++];
cmds.push("L");
crds.push(x, y);
} else if (cmu == "C") {
var x1 = xi + ts[i++], y1 = yi + ts[i++], x2 = xi + ts[i++], y2 = yi + ts[i++], x3 = xi + ts[i++], y3 = yi + ts[i++];
cmds.push("C");
crds.push(x1, y1, x2, y2, x3, y3);
x = x3;
y = y3;
} else if (cmu == "S") {
var co = Math.max(crds.length - 4, 0);
var x1 = x + x - crds[co], y1 = y + y - crds[co + 1];
var x2 = xi + ts[i++], y2 = yi + ts[i++], x3 = xi + ts[i++], y3 = yi + ts[i++];
cmds.push("C");
crds.push(x1, y1, x2, y2, x3, y3);
x = x3;
y = y3;
} else console.log("Unknown SVG command " + cmd);
}
}
}
};
Typr.SVG._reps = function(ts, off, ps) {
var i = off;
while (i < ts.length) {
if (typeof ts[i] == "string") break;
i += ps;
}
return (i - off) / ps;
};
if (Typr == null) Typr = {};
if (Typr.U == null) Typr.U = {};
Typr.U.codeToGlyph = function(font, code) {
var cmap = font.cmap;
var tind = -1;
if (cmap.p0e4 != null) tind = cmap.p0e4;
else if (cmap.p3e1 != null) tind = cmap.p3e1;
else if (cmap.p1e0 != null) tind = cmap.p1e0;
if (tind == -1) throw "no familiar platform and encoding!";
var tab = cmap.tables[tind];
if (tab.format == 0) {
if (code >= tab.map.length) return 0;
return tab.map[code];
} else if (tab.format == 4) {
var sind = -1;
for (var i = 0; i < tab.endCount.length; i++) if (code <= tab.endCount[i]) {
sind = i;
break;
}
if (sind == -1) return 0;
if (tab.startCount[sind] > code) return 0;
var gli = 0;
if (tab.idRangeOffset[sind] != 0) gli = tab.glyphIdArray[code - tab.startCount[sind] + (tab.idRangeOffset[sind] >> 1) - (tab.idRangeOffset.length - sind)];
else gli = code + tab.idDelta[sind];
return gli & 65535;
} else if (tab.format == 12) {
if (code > tab.groups[tab.groups.length - 1][1]) return 0;
for (var i = 0; i < tab.groups.length; i++) {
var grp = tab.groups[i];
if (grp[0] <= code && code <= grp[1]) return grp[2] + (code - grp[0]);
}
return 0;
} else throw "unknown cmap table format " + tab.format;
};
Typr.U.glyphToPath = function(font, gid) {
var path = { cmds: [], crds: [] };
if (font.SVG && font.SVG.entries[gid]) {
var p = font.SVG.entries[gid];
if (p == null) return path;
if (typeof p == "string") {
p = Typr.SVG.toPath(p);
font.SVG.entries[gid] = p;
}
return p;
} else if (font.CFF) {
var state = { x: 0, y: 0, stack: [], nStems: 0, haveWidth: false, width: font.CFF.Private ? font.CFF.Private.defaultWidthX : 0, open: false };
Typr.U._drawCFF(font.CFF.CharStrings[gid], state, font.CFF, path);
} else if (font.glyf) {
Typr.U._drawGlyf(gid, font, path);
}
return path;
};
Typr.U._drawGlyf = function(gid, font, path) {
var gl = font.glyf[gid];
if (gl == null) gl = font.glyf[gid] = Typr.glyf._parseGlyf(font, gid);
if (gl != null) {
if (gl.noc > -1) Typr.U._simpleGlyph(gl, path);
else Typr.U._compoGlyph(gl, font, path);
}
};
Typr.U._simpleGlyph = function(gl, p) {
for (var c = 0; c < gl.noc; c++) {
var i0 = c == 0 ? 0 : gl.endPts[c - 1] + 1;
var il = gl.endPts[c];
for (var i = i0; i <= il; i++) {
var pr = i == i0 ? il : i - 1;
var nx = i == il ? i0 : i + 1;
var onCurve = gl.flags[i] & 1;
var prOnCurve = gl.flags[pr] & 1;
var nxOnCurve = gl.flags[nx] & 1;
var x = gl.xs[i], y = gl.ys[i];
if (i == i0) {
if (onCurve) {
if (prOnCurve) Typr.U.P.moveTo(p, gl.xs[pr], gl.ys[pr]);
else {
Typr.U.P.moveTo(p, x, y);
continue;
}
} else {
if (prOnCurve) Typr.U.P.moveTo(p, gl.xs[pr], gl.ys[pr]);
else Typr.U.P.moveTo(p, (gl.xs[pr] + x) / 2, (gl.ys[pr] + y) / 2);
}
}
if (onCurve) {
if (prOnCurve) Typr.U.P.lineTo(p, x, y);
} else {
if (nxOnCurve) Typr.U.P.qcurveTo(p, x, y, gl.xs[nx], gl.ys[nx]);
else Typr.U.P.qcurveTo(p, x, y, (x + gl.xs[nx]) / 2, (y + gl.ys[nx]) / 2);
}
}
Typr.U.P.closePath(p);
}
};
Typr.U._compoGlyph = function(gl, font, p) {
for (var j = 0; j < gl.parts.length; j++) {
var path = { cmds: [], crds: [] };
var prt = gl.parts[j];
Typr.U._drawGlyf(prt.glyphIndex, font, path);
var m = prt.m;
for (var i = 0; i < path.crds.length; i += 2) {
var x = path.crds[i], y = path.crds[i + 1];
p.crds.push(x * m.a + y * m.b + m.tx);
p.crds.push(x * m.c + y * m.d + m.ty);
}
for (var i = 0; i < path.cmds.length; i++) p.cmds.push(path.cmds[i]);
}
};
Typr.U._getGlyphClass = function(g, cd) {
var intr = Typr._lctf.getInterval(cd, g);
return intr == -1 ? 0 : cd[intr + 2];
};
Typr.U.getPairAdjustment = function(font, g1, g2) {
if (font.GPOS) {
var ltab = null;
for (var i = 0; i < font.GPOS.featureList.length; i++) {
var fl = font.GPOS.featureList[i];
if (fl.tag == "kern") {
for (var j = 0; j < fl.tab.length; j++) if (font.GPOS.lookupList[fl.tab[j]].ltype == 2) ltab = font.GPOS.lookupList[fl.tab[j]];
}
}
if (ltab) {
for (var i = 0; i < ltab.tabs.length; i++) {
var tab = ltab.tabs[i];
var ind = Typr._lctf.coverageIndex(tab.coverage, g1);
if (ind == -1) continue;
var adj;
if (tab.format == 1) {
var right = tab.pairsets[ind];
for (var j = 0; j < right.length; j++) if (right[j].gid2 == g2) adj = right[j];
if (adj == null) continue;
} else if (tab.format == 2) {
var c1 = Typr.U._getGlyphClass(g1, tab.classDef1);
var c2 = Typr.U._getGlyphClass(g2, tab.classDef2);
var adj = tab.matrix[c1][c2];
}
return adj.val1[2];
}
}
}
if (font.kern) {
var ind1 = font.kern.glyph1.indexOf(g1);
if (ind1 != -1) {
var ind2 = font.kern.rval[ind1].glyph2.indexOf(g2);
if (ind2 != -1) return font.kern.rval[ind1].vals[ind2];
}
}
return 0;
};
Typr.U.stringToGlyphs = function(font, str) {
var gls = [];
for (var i = 0; i < str.length; i++) {
var cc = str.codePointAt(i);
if (cc > 65535) i++;
gls.push(Typr.U.codeToGlyph(font, cc));
}
var gsub = font["GSUB"];
if (gsub == null) return gls;
var llist = gsub.lookupList, flist = gsub.featureList;
var wsep = '\n " ,.:;!?() ،';
var R = "آأؤإاةدذرزوٱٲٳٵٶٷڈډڊڋڌڍڎڏڐڑڒړڔڕږڗژڙۀۃۄۅۆۇۈۉۊۋۍۏےۓەۮۯܐܕܖܗܘܙܞܨܪܬܯݍݙݚݛݫݬݱݳݴݸݹࡀࡆࡇࡉࡔࡧࡩࡪࢪࢫࢬࢮࢱࢲࢹૅેૉૐૡ૯ஃஅஉஎஏனப";
var L = "ꡲ્";
for (var ci = 0; ci < gls.length; ci++) {
var gl = gls[ci];
var slft = ci == 0 || wsep.indexOf(str[ci - 1]) != -1;
var srgt = ci == gls.length - 1 || wsep.indexOf(str[ci + 1]) != -1;
if (!slft && R.indexOf(str[ci - 1]) != -1) slft = true;
if (!srgt && R.indexOf(str[ci]) != -1) srgt = true;
if (!srgt && L.indexOf(str[ci + 1]) != -1) srgt = true;
if (!slft && L.indexOf(str[ci]) != -1) slft = true;
var feat = null;
if (slft) feat = srgt ? "isol" : "init";
else feat = srgt ? "fina" : "medi";
for (var fi = 0; fi < flist.length; fi++) {
if (flist[fi].tag != feat) continue;
for (var ti = 0; ti < flist[fi].tab.length; ti++) {
var tab = llist[flist[fi].tab[ti]];
if (tab.ltype != 1) continue;
Typr.U._applyType1(gls, ci, tab);
}
}
}
var cligs = ["rlig", "liga", "mset"];
for (var ci = 0; ci < gls.length; ci++) {
var gl = gls[ci];
var rlim = Math.min(3, gls.length - ci - 1);
for (var fi = 0; fi < flist.length; fi++) {
var fl = flist[fi];
if (cligs.indexOf(fl.tag) == -1) continue;
for (var ti = 0; ti < fl.tab.length; ti++) {
var tab = llist[fl.tab[ti]];
for (var j = 0; j < tab.tabs.length; j++) {
if (tab.tabs[j] == null) continue;
var ind = Typr._lctf.coverageIndex(tab.tabs[j].coverage, gl);
if (ind == -1) continue;
if (tab.ltype == 4) {
var vals = tab.tabs[j].vals[ind];
for (var k = 0; k < vals.length; k++) {
var lig = vals[k], rl = lig.chain.length;
if (rl > rlim) continue;
var good = true;
for (var l = 0; l < rl; l++) if (lig.chain[l] != gls[ci + (1 + l)]) good = false;
if (!good) continue;
gls[ci] = lig.nglyph;
for (var l = 0; l < rl; l++) gls[ci + l + 1] = -1;
}
} else if (tab.ltype == 5) {
var ltab = tab.tabs[j];
if (ltab.fmt != 2) continue;
var cind = Typr._lctf.getInterval(ltab.cDef, gl);
var cls = ltab.cDef[cind + 2], scs = ltab.scset[cls];
for (var i = 0; i < scs.length; i++) {
var sc = scs[i], inp = sc.input;
if (inp.length > rlim) continue;
var good = true;
for (var l = 0; l < inp.length; l++) {
var cind2 = Typr._lctf.getInterval(ltab.cDef, gls[ci + 1 + l]);
if (cind == -1 && ltab.cDef[cind2 + 2] != inp[l]) {
good = false;
break;
}
}
if (!good) continue;
var lrs = sc.substLookupRecords;
for (var k = 0; k < lrs.length; k += 2) {
lrs[k];
lrs[k + 1];
}
}
}
}
}
}
}
return gls;
};
Typr.U._applyType1 = function(gls, ci, tab) {
var gl = gls[ci];
for (var j = 0; j < tab.tabs.length; j++) {
var ttab = tab.tabs[j];
var ind = Typr._lctf.coverageIndex(ttab.coverage, gl);
if (ind == -1) continue;
if (ttab.fmt == 1) gls[ci] = gls[ci] + ttab.delta;
else gls[ci] = ttab.newg[ind];
}
};
Typr.U.glyphsToPath = function(font, gls, clr) {
var tpath = { cmds: [], crds: [] };
var x = 0;
for (var i = 0; i < gls.length; i++) {
var gid = gls[i];
if (gid == -1) continue;
var gid2 = i < gls.length - 1 && gls[i + 1] != -1 ? gls[i + 1] : 0;
var path = Typr.U.glyphToPath(font, gid);
for (var j = 0; j < path.crds.length; j += 2) {
tpath.crds.push(path.crds[j] + x);
tpath.crds.push(path.crds[j + 1]);
}
if (clr) tpath.cmds.push(clr);
for (var j = 0; j < path.cmds.length; j++) tpath.cmds.push(path.cmds[j]);
if (clr) tpath.cmds.push("X");
x += font.hmtx.aWidth[gid];
if (i < gls.length - 1) x += Typr.U.getPairAdjustment(font, gid, gid2);
}
return tpath;
};
Typr.U.pathToSVG = function(path, prec) {
if (prec == null) prec = 5;
var out = [], co = 0, lmap = { M: 2, L: 2, Q: 4, C: 6 };
for (var i = 0; i < path.cmds.length; i++) {
var cmd = path.cmds[i], cn = co + (lmap[cmd] ? lmap[cmd] : 0);
out.push(cmd);
while (co < cn) {
var c = path.crds[co++];
out.push(parseFloat(c.toFixed(prec)) + (co == cn ? "" : " "));
}
}
return out.join("");
};
Typr.U.pathToContext = function(path, ctx) {
var c = 0, crds = path.crds;
for (var j = 0; j < path.cmds.length; j++) {
var cmd = path.cmds[j];
if (cmd == "M") {
ctx.moveTo(crds[c], crds[c + 1]);
c += 2;
} else if (cmd == "L") {
ctx.lineTo(crds[c], crds[c + 1]);
c += 2;
} else if (cmd == "C") {
ctx.bezierCurveTo(crds[c], crds[c + 1], crds[c + 2], crds[c + 3], crds[c + 4], crds[c + 5]);
c += 6;
} else if (cmd == "Q") {
ctx.quadraticCurveTo(crds[c], crds[c + 1], crds[c + 2], crds[c + 3]);
c += 4;
} else if (cmd.charAt(0) == "#") {
ctx.beginPath();
ctx.fillStyle = cmd;
} else if (cmd == "Z") {
ctx.closePath();
} else if (cmd == "X") {
ctx.fill();
}
}
};
Typr.U.P = {};
Typr.U.P.moveTo = function(p, x, y) {
p.cmds.push("M");
p.crds.push(x, y);
};
Typr.U.P.lineTo = function(p, x, y) {
p.cmds.push("L");
p.crds.push(x, y);
};
Typr.U.P.curveTo = function(p, a, b, c, d, e, f) {
p.cmds.push("C");
p.crds.push(a, b, c, d, e, f);
};
Typr.U.P.qcurveTo = function(p, a, b, c, d) {
p.cmds.push("Q");
p.crds.push(a, b, c, d);
};
Typr.U.P.closePath = function(p) {
p.cmds.push("Z");
};
Typr.U._drawCFF = function(cmds, state, font, p) {
var stack = state.stack;
var nStems = state.nStems, haveWidth = state.haveWidth, width = state.width, open = state.open;
var i = 0;
var x = state.x, y = state.y, c1x = 0, c1y = 0, c2x = 0, c2y = 0, c3x = 0, c3y = 0, c4x = 0, c4y = 0, jpx = 0, jpy = 0;
var o = { val: 0, size: 0 };
while (i < cmds.length) {
Typr.CFF.getCharString(cmds, i, o);
var v = o.val;
i += o.size;
if (v == "o1" || v == "o18") {
var hasWidthArg;
hasWidthArg = stack.length % 2 !== 0;
if (hasWidthArg && !haveWidth) {
width = stack.shift() + font.Private.nominalWidthX;
}
nStems += stack.length >> 1;
stack.length = 0;
haveWidth = true;
} else if (v == "o3" || v == "o23") {
var hasWidthArg;
hasWidthArg = stack.length % 2 !== 0;
if (hasWidthArg && !haveWidth) {
width = stack.shift() + font.Private.nominalWidthX;
}
nStems += stack.length >> 1;
stack.length = 0;
haveWidth = true;
} else if (v == "o4") {
if (stack.length > 1 && !haveWidth) {
width = stack.shift() + font.Private.nominalWidthX;
haveWidth = true;
}
if (open) Typr.U.P.closePath(p);
y += stack.pop();
Typr.U.P.moveTo(p, x, y);
open = true;
} else if (v == "o5") {
while (stack.length > 0) {
x += stack.shift();
y += stack.shift();
Typr.U.P.lineTo(p, x, y);
}
} else if (v == "o6" || v == "o7") {
var count2 = stack.length;
var isX = v == "o6";
for (var j = 0; j < count2; j++) {
var sval = stack.shift();
if (isX) x += sval;
else y += sval;
isX = !isX;
Typr.U.P.lineTo(p, x, y);
}
} else if (v == "o8" || v == "o24") {
var count2 = stack.length;
var index = 0;
while (index + 6 <= count2) {
c1x = x + stack.shift();
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
x = c2x + stack.shift();
y = c2y + stack.shift();
Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, x, y);
index += 6;
}
if (v == "o24") {
x += stack.shift();
y += stack.shift();
Typr.U.P.lineTo(p, x, y);
}
} else if (v == "o11") break;
else if (v == "o1234" || v == "o1235" || v == "o1236" || v == "o1237") {
if (v == "o1234") {
c1x = x + stack.shift();
c1y = y;
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
jpx = c2x + stack.shift();
jpy = c2y;
c3x = jpx + stack.shift();
c3y = c2y;
c4x = c3x + stack.shift();
c4y = y;
x = c4x + stack.shift();
Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
Typr.U.P.curveTo(p, c3x, c3y, c4x, c4y, x, y);
}
if (v == "o1235") {
c1x = x + stack.shift();
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
jpx = c2x + stack.shift();
jpy = c2y + stack.shift();
c3x = jpx + stack.shift();
c3y = jpy + stack.shift();
c4x = c3x + stack.shift();
c4y = c3y + stack.shift();
x = c4x + stack.shift();
y = c4y + stack.shift();
stack.shift();
Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
Typr.U.P.curveTo(p, c3x, c3y, c4x, c4y, x, y);
}
if (v == "o1236") {
c1x = x + stack.shift();
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
jpx = c2x + stack.shift();
jpy = c2y;
c3x = jpx + stack.shift();
c3y = c2y;
c4x = c3x + stack.shift();
c4y = c3y + stack.shift();
x = c4x + stack.shift();
Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
Typr.U.P.curveTo(p, c3x, c3y, c4x, c4y, x, y);
}
if (v == "o1237") {
c1x = x + stack.shift();
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
jpx = c2x + stack.shift();
jpy = c2y + stack.shift();
c3x = jpx + stack.shift();
c3y = jpy + stack.shift();
c4x = c3x + stack.shift();
c4y = c3y + stack.shift();
if (Math.abs(c4x - x) > Math.abs(c4y - y)) {
x = c4x + stack.shift();
} else {
y = c4y + stack.shift();
}
Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
Typr.U.P.curveTo(p, c3x, c3y, c4x, c4y, x, y);
}
} else if (v == "o14") {
if (stack.length > 0 && !haveWidth) {
width = stack.shift() + font.nominalWidthX;
haveWidth = true;
}
if (stack.length == 4) {
var adx = stack.shift();
var ady = stack.shift();
var bchar = stack.shift();
var achar = stack.shift();
var bind = Typr.CFF.glyphBySE(font, bchar);
var aind = Typr.CFF.glyphBySE(font, achar);
Typr.U._drawCFF(font.CharStrings[bind], state, font, p);
state.x = adx;
state.y = ady;
Typr.U._drawCFF(font.CharStrings[aind], state, font, p);
}
if (open) {
Typr.U.P.closePath(p);
open = false;
}
} else if (v == "o19" || v == "o20") {
var hasWidthArg;
hasWidthArg = stack.length % 2 !== 0;
if (hasWidthArg && !haveWidth) {
width = stack.shift() + font.Private.nominalWidthX;
}
nStems += stack.length >> 1;
stack.length = 0;
haveWidth = true;
i += nStems + 7 >> 3;
} else if (v == "o21") {
if (stack.length > 2 && !haveWidth) {
width = stack.shift() + font.Private.nominalWidthX;
haveWidth = true;
}
y += stack.pop();
x += stack.pop();
if (open) Typr.U.P.closePath(p);
Typr.U.P.moveTo(p, x, y);
open = true;
} else if (v == "o22") {
if (stack.length > 1 && !haveWidth) {
width = stack.shift() + font.Private.nominalWidthX;
haveWidth = true;
}
x += stack.pop();
if (open) Typr.U.P.closePath(p);
Typr.U.P.moveTo(p, x, y);
open = true;
} else if (v == "o25") {
while (stack.length > 6) {
x += stack.shift();
y += stack.shift();
Typr.U.P.lineTo(p, x, y);
}
c1x = x + stack.shift();
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
x = c2x + stack.shift();
y = c2y + stack.shift();
Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, x, y);
} else if (v == "o26") {
if (stack.length % 2) {
x += stack.shift();
}
while (stack.length > 0) {
c1x = x;
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
x = c2x;
y = c2y + stack.shift();
Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, x, y);
}
} else if (v == "o27") {
if (stack.length % 2) {
y += stack.shift();
}
while (stack.length > 0) {
c1x = x + stack.shift();
c1y = y;
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
x = c2x + stack.shift();
y = c2y;
Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, x, y);
}
} else if (v == "o10" || v == "o29") {
var obj = v == "o10" ? font.Private : font;
if (stack.length == 0) {
console.log("error: empty stack");
} else {
var ind = stack.pop();
var subr = obj.Subrs[ind + obj.Bias];
state.x = x;
state.y = y;
state.nStems = nStems;
state.haveWidth = haveWidth;
state.width = width;
state.open = open;
Typr.U._drawCFF(subr, state, font, p);
x = state.x;
y = state.y;
nStems = state.nStems;
haveWidth = state.haveWidth;
width = state.width;
open = state.open;
}
} else if (v == "o30" || v == "o31") {
var count2, count1 = stack.length;
var index = 0;
var alternate = v == "o31";
count2 = count1 & -3;
index += count1 - count2;
while (index < count2) {
if (alternate) {
c1x = x + stack.shift();
c1y = y;
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
y = c2y + stack.shift();
if (count2 - index == 5) {
x = c2x + stack.shift();
index++;
} else x = c2x;
alternate = false;
} else {
c1x = x;
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
x = c2x + stack.shift();
if (count2 - index == 5) {
y = c2y + stack.shift();
index++;
} else y = c2y;
alternate = true;
}
Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, x, y);
index += 4;
}
} else if ((v + "").charAt(0) == "o") {
console.log("Unknown operation: " + v, cmds);
throw v;
} else stack.push(v);
}
state.x = x;
state.y = y;
state.nStems = nStems;
state.haveWidth = haveWidth;
state.width = width;
state.open = open;
};
var typr_js = Typr;
const Typr$1 = /* @__PURE__ */ getDefaultExportFromCjs(typr_js);
const MESSAGE$1 = {
[AiAskCode.Invalid]: "用户名、密码或人机验证无效",
[AiAskCode.Unauthorized]: "用户名或密码错误",
[AiAskCode.RateLimited]: "操作太频繁,请稍后再试",
[AiAskCode.Busy]: "服务繁忙,请稍后重试"
};
function registerPrecheck(username, password, email) {
if (username.length < 3 || username.length > 32)
return `用户名要 3-32 位,现在是 ${username.length} 位`;
if (password.length < 8) return `密码至少 8 位,现在是 ${password.length} 位`;
if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(email))
return "邮箱格式不对;不想填就留空";
return null;
}
async function authenticate(transport, mode, username, password, baseUrl, captchaToken, email) {
const trimmedEmail = (email == null ? void 0 : email.trim()) ?? "";
if (!username) return { message: "请输入用户名" };
if (!password) return { message: "请输入密码" };
if (mode === "register") {
const problem = registerPrecheck(username, password, trimmedEmail);
if (problem) return { message: problem };
}
const verifiedCaptchaToken = captchaToken == null ? void 0 : captchaToken.trim();
if (mode === "register" && !verifiedCaptchaToken)
return { message: "请先完成人机验证" };
try {
const res = await transport.send({
url: baseUrl + (mode === "register" ? AUTH_REGISTER_PATH : AUTH_LOGIN_PATH),
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
v: SCRIPT_VERSION
},
body: JSON.stringify(
mode === "register" ? {
username,
password,
captchaToken: verifiedCaptchaToken,
// 「没填」必须表现为**键不存在**,不能是空串:后端那条 schema 是
// `z.string().email().optional()`,空串过不了 `.email()`,
// 整笔注册会被判 Invalid 而失败——用户只会看到「用户名、密码或
// 人机验证无效」,完全指不到邮箱这一栏上。
...trimmedEmail ? { email: trimmedEmail } : {}
} : { username, password }
),
timeoutMs: 8e3
});
const parsed = AuthResponseSchema.safeParse(JSON.parse(res.body));
if (!parsed.success) return { message: MESSAGE$1[AiAskCode.Busy] };
const { code, token, reason } = parsed.data;
if (code === AiAskCode.Ok && token) return { token, message: "ok" };
if (reason === "taken")
return {
message: trimmedEmail ? "用户名或邮箱已被占用,换一个再试" : "用户名已被占用,换一个再试"
};
if (reason === "disabled")
return { message: "该账号已被禁用,密码是对的。请联系客服处理" };
return { message: MESSAGE$1[code] ?? MESSAGE$1[AiAskCode.Busy] };
} catch {
return { message: MESSAGE$1[AiAskCode.Busy] };
}
}
const ALL_KINDS = [
"media",
"chapter-test",
"document",
"ppt-audio",
"timed-read",
"hyperlink",
"flash",
"unknown"
];
const isTaskKind = (value) => ALL_KINDS.includes(value);
const TASK_TOGGLES = [
"media",
"chapter-test",
"reading",
"hyperlink"
];
const TOGGLE_LABEL = {
media: "视频与音频",
"chapter-test": "章节测验",
reading: "PPT / 文档 / 书籍",
hyperlink: "链接"
};
const KIND_TOGGLE = {
media: "media",
"chapter-test": "chapter-test",
document: "reading",
"ppt-audio": "reading",
"timed-read": "reading",
flash: "reading",
hyperlink: "hyperlink",
unknown: null
};
const toggleForKind = (kind) => KIND_TOGGLE[kind];
const KIND_LABEL = {
media: "视频与音频",
"chapter-test": "章节测验",
// 探针是 `#img.imglook`(图片阅读),但兜底还收 `insertdoc` / `insertbook`。
// 原标签「文档」承诺的范围比探针大得多——用户勾掉它以为关掉了所有课件,
// 实际只关掉了一种。标签要么配得上判据,要么把判据补齐;这里选后者。
document: "文档与书籍",
"ppt-audio": "带音频课件",
"timed-read": "长时阅读",
hyperlink: "链接",
flash: "Flash 动画",
unknown: "未知类型"
};
const TASK_SKIP_LABEL = {
passed: "站点标记已播完",
"not-a-job": "站点未计为任务点",
"test-done": "页面标记测验已完成",
"section-clear": "站点清单已无待办",
"marked-done": "任务点已完成标记",
"kind-off": "该类型已被你关闭",
handled: "本节内已处理过"
};
const isPendingTask = (task) => task.skip === null;
const DEFAULT_COURSE_CONFIG = Object.freeze({
probes: Object.freeze([
Object.freeze(["media", "#video, #audio"]),
Object.freeze(["chapter-test", ".TiMu"]),
Object.freeze([
"timed-read",
'iframe[name="bookifame"][src*="timing"]'
]),
Object.freeze(["ppt-audio", ".swiper-container"]),
Object.freeze(["document", "#img.imglook"]),
Object.freeze(["hyperlink", "#hyperlink"]),
// 播放器容器 id 改版也还认得出媒体。必须垫底,理由见文件头。
Object.freeze(["media", "video, audio"])
]),
moduleKind: Object.freeze({
insertvideo: "media",
insertaudio: "media",
insertdoc: "document",
insertbook: "document",
insertflash: "flash",
work: "chapter-test",
insertimage: "document"
}),
faceLegacy: "#fcqrimg",
faceMask: ".chapterVideoFaceMaskDiv",
videoQuiz: "#videoquiz-submit",
playerError: ".vjs-modal-dialog-content",
playerErrorTexts: Object.freeze([
"视频文件损坏",
"网络错误导致视频下载中途失败",
"视频因格式不支持",
"网络的问题无法加载"
]),
taskDoneText: "任务点已完成",
// 2026-08-02 真页确证:未完成的章测是 `class="fr testTit_status"`,完成时追加
// `testTit_status_complete`。判据同 OCS `cx.ts:1571`。
// 与云端规则 match 里的 `findQuestionFrameStep` 逐字同源,改一处要一起改。
chapterTestAnswerable: '.TiMu input[name^="answertype"]',
chapterTestStatus: ".testTit_status",
chapterTestDoneClass: "testTit_status_complete",
// 真页上未完成的状态条写「待完成」,它不含「已完成」,不会互相误伤。
chapterTestDoneText: "已完成",
// 交了但还没批的形态。真页 2026-08-20:状态条文本恰为「待批阅」,同屏的任务点块
// 已经标着「任务点已完成」——站点自己认完成,只有我们的判据不认。
chapterTestSubmittedTexts: Object.freeze(["待批阅", "已提交"]),
taskTab: ".prev_ul li",
chapter: '[onclick^="getTeacherAjax"]',
jobUnfinishCount: ".jobUnfinishCount",
chapterName: ".posCatalog_name",
specialMode: ".catalog_points_sa, .catalog_points_er",
cursorCourseId: "#curCourseId",
cursorChapterId: "#curChapterId",
cursorClazzId: "#curClazzId",
sectionTabs: "#prev_tab .prev_ul li",
nextSectionFallback: ".nodeItem.r i",
bigPlay: ".vjs-big-play-button",
bigPlayLabel: "播放视频",
readerPager: ".readerPager",
activePagerZIndex: "101",
pptSlide: ".swiper-container .swiper-slide",
timedReadFrame: 'iframe[name="bookifame"][src*="timing"]'
});
const SELECTOR_KEYS = Object.freeze({
"course.gate.faceLegacy": "faceLegacy",
"course.gate.faceMask": "faceMask",
"course.gate.videoQuiz": "videoQuiz",
"course.gate.playerError": "playerError",
"course.marker.taskDone": "taskDoneText",
"course.probe.chapterTestAnswerable": "chapterTestAnswerable",
"course.marker.chapterTestStatus": "chapterTestStatus",
"course.marker.chapterTestDoneClass": "chapterTestDoneClass",
"course.marker.chapterTestDoneText": "chapterTestDoneText",
"course.nav.taskTab": "taskTab",
"course.nav.chapter": "chapter",
"course.nav.jobUnfinishCount": "jobUnfinishCount",
"course.nav.chapterName": "chapterName",
"course.nav.specialMode": "specialMode",
"course.nav.cursorCourseId": "cursorCourseId",
"course.nav.cursorChapterId": "cursorChapterId",
"course.nav.cursorClazzId": "cursorClazzId",
"course.nav.sectionTabs": "sectionTabs",
"course.nav.nextSectionFallback": "nextSectionFallback",
"course.action.bigPlay": "bigPlay",
"course.action.bigPlayLabel": "bigPlayLabel",
"course.reader.pager": "readerPager",
"course.reader.pagerZIndex": "activePagerZIndex",
"course.reader.pptSlide": "pptSlide",
"course.reader.timedReadFrame": "timedReadFrame"
});
const PROBE_PREFIX = "course.probe.";
const MODULE_PREFIX = "course.module.";
const SUBMITTED_TEXTS_KEY = "course.marker.chapterTestSubmittedTexts";
const ERROR_TEXTS_KEY = "course.gate.playerErrorTexts";
const usableSelector = (value, probe) => {
try {
probe.createDocumentFragment().querySelector(value);
return true;
} catch {
return false;
}
};
const firstString = (value) => typeof value === "string" && value.trim() ? value : null;
function resolveCourseConfig(remote, probe = globalThis.document) {
if (!remote || typeof remote !== "object" || !probe)
return DEFAULT_COURSE_CONFIG;
const table = remote;
const next = { ...DEFAULT_COURSE_CONFIG };
for (const [key, field] of Object.entries(SELECTOR_KEYS)) {
const value = firstString(table[key]);
if (value === null) continue;
const isSelector = field !== "taskDoneText" && field !== "bigPlayLabel" && field !== "activePagerZIndex" && field !== "chapterTestDoneClass" && field !== "chapterTestDoneText";
if (isSelector && !usableSelector(value, probe)) continue;
next[field] = value;
}
const errorTexts = table[ERROR_TEXTS_KEY];
if (Array.isArray(errorTexts)) {
const texts = errorTexts.filter(
(item) => typeof item === "string" && !!item.trim()
);
if (texts.length > 0) next.playerErrorTexts = Object.freeze(texts);
}
const submittedTexts = table[SUBMITTED_TEXTS_KEY];
if (Array.isArray(submittedTexts)) {
const texts = submittedTexts.filter(
(item) => typeof item === "string" && !!item.trim()
);
if (texts.length > 0) next.chapterTestSubmittedTexts = Object.freeze(texts);
}
next.probes = Object.freeze(
DEFAULT_COURSE_CONFIG.probes.map(([kind, selector], index) => {
const override = firstString(table[`${PROBE_PREFIX}${kind}.${index}`]);
return Object.freeze([
kind,
override && usableSelector(override, probe) ? override : selector
]);
})
);
const moduleKind = {
...DEFAULT_COURSE_CONFIG.moduleKind
};
for (const [key, value] of Object.entries(table)) {
if (!key.startsWith(MODULE_PREFIX)) continue;
const name = key.slice(MODULE_PREFIX.length);
const kind = firstString(value);
if (!name || !kind || !isTaskKind(kind)) continue;
moduleKind[name] = kind;
}
next.moduleKind = Object.freeze(moduleKind);
return Object.freeze(next);
}
let active = DEFAULT_COURSE_CONFIG;
const courseConfig = () => active;
function applyCourseConfig(remote, probe) {
active = resolveCourseConfig(remote, probe);
return active;
}
function activeMedia(documents) {
for (const doc of documents)
for (const candidate of doc.querySelectorAll("video, audio")) {
const media = candidate;
if (!media.paused && media.readyState > 0) return media;
}
return null;
}
function mediaPosition(media) {
return {
currentSeconds: media.currentTime,
totalSeconds: Number.isFinite(media.duration) ? media.duration : null,
rate: media.playbackRate
};
}
const WILL_NOT_BE_DONE = /* @__PURE__ */ new Set(["kind-off", "not-a-job"]);
function sectionLayer(survey, skipped) {
if (!survey.authoritative) return null;
const offCount = skipped.filter((item) => item.reason === "kind-off").length;
const total = Math.max(0, survey.declared - offCount);
const pending = Math.max(
0,
survey.tasks.filter(isPendingTask).length - offCount
);
return {
done: Math.max(0, total - pending),
total,
// 计数(`offCount`)用的是完整清单,展示用的只留「不会被做」的那些——见 WILL_NOT_BE_DONE。
skipped: skipped.filter((item) => WILL_NOT_BE_DONE.has(item.reason)).map((item) => ({
name: item.name,
kind: item.kind,
reason: item.reason
}))
};
}
function courseProgress(documents, survey, skipped, activeTask, course) {
const media = activeMedia(documents);
return {
task: activeTask ? {
name: activeTask.name,
kind: activeTask.kind,
position: media ? mediaPosition(media) : null
} : null,
section: sectionLayer(survey, skipped),
course
};
}
const MAX_READ_FRAMES = 64;
const MAX_READ_DEPTH = 8;
function readableDocuments(root) {
const out = [root];
const seen = /* @__PURE__ */ new Set([root]);
const queue = [
{ doc: root, depth: 0 }
];
let frames = 0;
while (queue.length > 0) {
const current = queue.shift();
if (!current || current.depth >= MAX_READ_DEPTH) continue;
let list = [];
try {
list = [...current.doc.querySelectorAll("iframe, frame")];
} catch {
continue;
}
for (const el of list) {
if (++frames > MAX_READ_FRAMES) return out;
let child = null;
try {
child = el.contentDocument;
} catch {
child = null;
}
if (!child || seen.has(child)) continue;
seen.add(child);
out.push(child);
queue.push({ doc: child, depth: current.depth + 1 });
}
}
return out;
}
const playableSource = (media) => !!(media.currentSrc || media.getAttribute("src") || media.querySelector("source[src]") || // 超星走 videojs,源可能由 MSE/blob 挂上去,此时 currentSrc 与 src 属性都可能是空的。
// 只认 src 会把真视频误判成「没有任务点」——比误播更难发现,因为它表现为「什么都没发生」。
// readyState ≥ HAVE_METADATA 说明媒体确实加载到了内容,而无源占位恒为 HAVE_NOTHING。
media.readyState >= 1);
const playableMediaList = (documents) => {
const found = [];
for (const doc of documents)
for (const candidate of doc.querySelectorAll("video, audio")) {
const media = candidate;
if (playableSource(media)) found.push(media);
}
return found;
};
const allMediaEnded = (document2) => {
const media = playableMediaList([document2]);
return media.length > 0 && media.every((item) => item.ended);
};
function skippedTasks(survey, options) {
const handled = options.isHandled ?? (() => false);
const kindEnabled = options.isKindEnabled ?? (() => true);
const out = [];
for (const task of survey.tasks) {
const reason = task.skip ? task.skip : !kindEnabled(task.kind) ? "kind-off" : task.kind === "media" ? allMediaEnded(task.document) ? "media-ended" : null : handled(task.key) ? "handled" : null;
if (reason)
out.push({ name: task.name, kind: task.kind, reason, key: task.key });
}
return out;
}
function pauseAllMedia(documents) {
let paused = false;
for (const doc of documents)
for (const el of doc.querySelectorAll("video, audio")) {
const media = el;
if (media.paused) continue;
try {
media.pause();
paused = true;
} catch {
}
}
return paused;
}
function pauseCourseMedia(document2) {
return pauseAllMedia(readableDocuments(document2));
}
const STOPPING_BLOCK_REASONS = /* @__PURE__ */ new Set(["budget-exhausted", "advance-failed", "locked"]);
function isRunnerStopped(state) {
if (state.kind === "course-done" || state.kind === "section-done") return true;
if (state.kind === "finished" || state.kind === "section-stalled") return true;
return state.kind === "blocked" && STOPPING_BLOCK_REASONS.has(state.reason);
}
const DEFAULT_INTERVAL_MS = 3e3;
const IDLE_TICKS_BEFORE_ADVANCE = 2;
const LOADING_TICKS_BEFORE_ADVANCE = 10;
const DEFAULT_MAX_DURATION_MS = 3 * 60 * 60 * 1e3;
const ANSWERING_TICKS_BUDGET = 60;
function runMediaTask(document2, options) {
const adapter = options.adapter;
const view = document2.defaultView;
if (!view) throw new Error("media task document has no window");
const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
const maxDurationMs = options.maxDurationMs ?? DEFAULT_MAX_DURATION_MS;
let elapsed = 0;
let idleTicks = 0;
let sectionsDone = 0;
let pendingAdvanceFrom = null;
let pendingTabFrom = null;
let readingTaskKey = null;
let readingSummary = null;
let lastSignature = null;
let lastSurveyKey = null;
const handled = /* @__PURE__ */ new Set();
const pptSteps = /* @__PURE__ */ new Map();
const answeringTicks = /* @__PURE__ */ new Map();
let dwellUntil = 0;
let dwellState = null;
let timer = null;
const stop = () => {
if (timer != null) view.clearInterval(timer);
timer = null;
};
const stepOptions = Object.create(options, {
isHandled: { value: (key) => handled.has(key) }
});
timer = view.setInterval(() => {
var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v;
const memoryPressure = (_a2 = options.memoryGuard) == null ? void 0 : _a2.check();
if (memoryPressure != null) {
stop();
(_b = options.onMemoryPressure) == null ? void 0 : _b.call(options, memoryPressure);
return;
}
elapsed += intervalMs;
if (elapsed > maxDurationMs) {
stop();
(_c = options.onState) == null ? void 0 : _c.call(options, { kind: "blocked", reason: "budget-exhausted" });
return;
}
const documents = ((_d = options.documents) == null ? void 0 : _d.call(options)) ?? readableDocuments(document2);
const readable = documents;
const signatureNow = adapter.navigate.sectionSignature(documents);
if (signatureNow !== lastSignature) {
lastSignature = signatureNow;
handled.clear();
pptSteps.clear();
answeringTicks.clear();
readingTaskKey = null;
}
if (options.onSurvey || options.onProgress) {
const survey = adapter.survey(documents);
const skipped = skippedTasks(survey, stepOptions);
if (options.onSurvey) {
const kinds = survey.tasks.map((task) => task.kind);
const key = `${kinds.join(",")}#${skipped.map((item) => `${item.name}:${item.reason}`).join("|")}`;
if (key !== lastSurveyKey) {
lastSurveyKey = key;
options.onSurvey({
frames: documents.length,
authoritative: survey.authoritative,
declared: survey.declared,
kinds,
pending: survey.tasks.filter(isPendingTask).length,
skipped
});
}
}
if (options.onProgress) {
const skippedKeys = new Set(skipped.map((item) => item.key));
const actionable = survey.tasks.find(
(item) => isPendingTask(item) && !skippedKeys.has(item.key)
);
options.onProgress(
courseProgress(
documents,
survey,
skipped,
actionable ?? null,
adapter.courseCounter(documents)
)
);
}
}
const tryAdvanceTab = (tabs2) => {
const tabKey = `${adapter.navigate.sectionSignature(documents)}#${tabs2.activeIndex}`;
if (pendingTabFrom === tabKey) {
pendingTabFrom = null;
return false;
}
if (!adapter.navigate.advanceTab(documents)) return false;
pendingTabFrom = tabKey;
return true;
};
if (dwellState && dwellUntil > elapsed) {
(_e = options.onState) == null ? void 0 : _e.call(options, { ...dwellState, remainingMs: dwellUntil - elapsed });
return;
}
dwellState = null;
const state = adapter.step(documents, stepOptions);
if (state.kind === "playing" || state.kind === "blocked") {
idleTicks = 0;
(_f = options.onState) == null ? void 0 : _f.call(options, state);
return;
}
if (state.kind === "dwelling") {
idleTicks = 0;
handled.add(state.taskKey);
dwellState = state;
dwellUntil = elapsed + state.remainingMs;
(_g = options.onState) == null ? void 0 : _g.call(options, state);
return;
}
if (state.kind === "answering") {
idleTicks = 0;
const spent = (answeringTicks.get(state.taskKey) ?? 0) + 1;
answeringTicks.set(state.taskKey, spent);
if (spent >= ANSWERING_TICKS_BUDGET || ((_h = options.isAnsweringDone) == null ? void 0 : _h.call(options, state.taskKey)))
handled.add(state.taskKey);
if (!state.frameLoaded) {
const tabs2 = adapter.navigate.tabs(documents);
if (tabs2 && tryAdvanceTab(tabs2)) {
(_i = options.onState) == null ? void 0 : _i.call(options, {
kind: "advancing",
toIndex: tabs2.activeIndex + 1
});
return;
}
}
(_j = options.onState) == null ? void 0 : _j.call(options, { ...state, ticks: spent });
return;
}
if (state.kind === "starting") {
idleTicks = 0;
(_k = options.onState) == null ? void 0 : _k.call(options, state);
return;
}
if (state.kind === "hyperlink") {
idleTicks = 0;
handled.add(state.taskKey);
(_l = options.onState) == null ? void 0 : _l.call(options, state);
return;
}
if (state.kind === "ppt-slide") {
idleTicks = 0;
const turned = (pptSteps.get(state.taskKey) ?? 0) + 1;
pptSteps.set(state.taskKey, turned);
if (turned >= Math.max(state.total, 1)) handled.add(state.taskKey);
(_m = options.onState) == null ? void 0 : _m.call(options, state);
return;
}
const tabs = adapter.navigate.tabs(documents);
const taskKey = state.kind === "idle" && state.taskKey ? state.taskKey : `${signatureNow}#${(tabs == null ? void 0 : tabs.activeIndex) ?? -1}`;
let scrolledNow = false;
if (state.kind === "idle" && readingTaskKey !== taskKey) {
const taskContext = state.taskKey != null || tabs !== null || adapter.navigate.sectionCursor(documents) !== null;
if (taskContext) {
readingTaskKey = taskKey;
readingSummary = adapter.simulateReading(readable);
scrolledNow = true;
if (state.taskKey) handled.add(state.taskKey);
}
}
if (state.kind === "idle" || state.kind === "loading") {
idleTicks += 1;
const grace = state.kind === "loading" ? LOADING_TICKS_BEFORE_ADVANCE : IDLE_TICKS_BEFORE_ADVANCE;
if (!tabs || idleTicks < grace) {
(_n = options.onState) == null ? void 0 : _n.call(
options,
scrolledNow && readingSummary ? { kind: "reading", summary: readingSummary } : state
);
return;
}
}
if (tabs && tryAdvanceTab(tabs)) {
idleTicks = 0;
(_o = options.onState) == null ? void 0 : _o.call(options, { kind: "advancing", toIndex: tabs.activeIndex + 1 });
return;
}
{
if (!tabs && !adapter.navigate.sectionCursor(documents)) {
if (state.kind !== "idle" && state.kind !== "loading") stop();
(_p = options.onState) == null ? void 0 : _p.call(options, state);
return;
}
const chapters = adapter.navigate.chapters(documents);
if (chapters.length > 0 && chapters.every((chapter2) => chapter2.unfinishedCount === 0)) {
stop();
(_q = options.onState) == null ? void 0 : _q.call(options, { kind: "course-done" });
return;
}
if (pendingAdvanceFrom !== null) {
if (signatureNow === pendingAdvanceFrom) {
const chapter2 = adapter.navigate.nextUnfinishedChapter(chapters);
if (chapter2 && adapter.navigate.jumpToChapter(documents, chapter2)) {
pendingAdvanceFrom = null;
idleTicks = 0;
(_r = options.onState) == null ? void 0 : _r.call(options, {
kind: "advancing-chapter",
name: adapter.navigate.chapterLabel(chapter2)
});
return;
}
stop();
(_s = options.onState) == null ? void 0 : _s.call(options, {
kind: "blocked",
// 闯关/解锁模式下推不动是「前置任务没做完」,不是选择器失效。
reason: adapter.navigate.isSpecialMode(documents) ? "locked" : "advance-failed"
});
return;
}
pendingAdvanceFrom = null;
}
if (adapter.navigate.advanceSection(documents)) {
sectionsDone += 1;
idleTicks = 0;
pendingAdvanceFrom = signatureNow;
(_t = options.onState) == null ? void 0 : _t.call(options, { kind: "advancing-section", sectionsDone });
return;
}
const chapter = adapter.navigate.nextUnfinishedChapter(chapters);
if (chapter && adapter.navigate.jumpToChapter(documents, chapter)) {
idleTicks = 0;
(_u = options.onState) == null ? void 0 : _u.call(options, {
kind: "advancing-chapter",
name: adapter.navigate.chapterLabel(chapter)
});
return;
}
stop();
(_v = options.onState) == null ? void 0 : _v.call(options, { kind: "section-done" });
return;
}
}, intervalMs);
return { stop };
}
const MAX_DEPTH = 12;
const MAX_KEYS = 40;
const MAX_TEXT = 8e3;
function looksLikeJson(value) {
const trimmed = value.trim();
return trimmed.startsWith("{") || trimmed.startsWith("[");
}
function describe(value, depth) {
if (value === null) return "null";
if (typeof value === "boolean") return "boolean";
if (typeof value === "number") return "number";
if (typeof value === "string") {
if (looksLikeJson(value) && depth < MAX_DEPTH) {
try {
return `string(json:${describe(JSON.parse(value), depth + 1)})`;
} catch {
return "string";
}
}
return "string";
}
if (typeof value !== "object") return typeof value;
if (depth >= MAX_DEPTH) return "…";
if (Array.isArray(value))
return value.length === 0 ? "array[0]" : `array[${value.length}] of ${describe(value[0], depth + 1)}`;
const keys = Object.keys(value);
const shown = keys.slice(0, MAX_KEYS).map((key) => {
const child = value[key];
return `${key}:${describe(child, depth + 1)}`;
});
if (keys.length > MAX_KEYS) shown.push(`…+${keys.length - MAX_KEYS}`);
return `{${shown.join(",")}}`;
}
function describeJsonShape(value) {
const text = describe(value, 0);
return text.length > MAX_TEXT ? `${text.slice(0, MAX_TEXT)}…(已截断)` : text;
}
const MAX_CAPTURE_CHARS = 2 * 1024 * 1024;
const HOOKED_SEND_FLAG = "__aiaskHookedXhrSend";
function isHookedSend(send) {
return typeof send === "function" && send[HOOKED_SEND_FLAG] === true;
}
function readPayload(xhr) {
if (xhr.responseType === "json") return xhr.response ?? null;
if (xhr.responseType && xhr.responseType !== "text") return null;
const raw = typeof xhr.response === "string" ? xhr.response : xhr.responseText ?? "";
if (!raw || raw.length > MAX_CAPTURE_CHARS) return null;
try {
return JSON.parse(raw);
} catch {
return null;
}
}
function createXhrResponseCapture(rules) {
const store = /* @__PURE__ */ new Map();
const slotFor = (responseURL) => {
let url;
try {
url = new URL(responseURL);
} catch {
return null;
}
const host = url.hostname.toLowerCase();
const match = rules.find(
(rule) => rule.host === host && url.pathname.includes(rule.pathIncludes)
);
return (match == null ? void 0 : match.slot) ?? null;
};
const consume = (xhr) => {
try {
if (xhr.readyState !== 4 || xhr.status !== 200) return;
const slot = slotFor(xhr.responseURL || "");
if (!slot) return;
const payload = readPayload(xhr);
if (payload == null) return;
store.set(slot, payload);
} catch {
}
};
return {
read: (slot) => store.get(slot) ?? null,
clear: () => store.clear(),
consume,
install(target) {
const descriptor = Object.getOwnPropertyDescriptor(
target.prototype,
"send"
);
const currentSend = (descriptor == null ? void 0 : descriptor.value) ?? target.prototype.send;
if (isHookedSend(currentSend)) return false;
if (typeof currentSend !== "function") return false;
const originalSend = currentSend;
const hookedSend = function(body) {
try {
this.addEventListener("readystatechange", () => consume(this));
} catch {
}
return originalSend.call(this, body);
};
Object.defineProperty(hookedSend, HOOKED_SEND_FLAG, { value: true });
try {
target.prototype.send = hookedSend;
} catch {
return false;
}
return isHookedSend(target.prototype.send);
}
};
}
const AOPENG_CAPTURE_HOST = "os.open.com.cn";
const [EXAM_VIEW_PAPER, EXAM_PULL_PAPER] = AOPENG_PAPER_SLOTS;
const AOPENG_CAPTURE_RULES = Object.freeze([
Object.freeze({
slot: EXAM_VIEW_PAPER,
host: AOPENG_CAPTURE_HOST,
pathIncludes: "/StudentViewPaper"
}),
Object.freeze({
slot: EXAM_PULL_PAPER,
host: AOPENG_CAPTURE_HOST,
pathIncludes: "/StudentPullPaper_V2"
})
]);
const aopengResponseCapture = createXhrResponseCapture(AOPENG_CAPTURE_RULES);
const capture = aopengResponseCapture;
function readAopengCapturedResponse(slot) {
return capture.read(slot);
}
let applicable = false;
let installTarget = "none";
let installed = false;
function dig(value, path) {
let current = value;
for (const key of path) {
if (typeof current === "string") {
try {
current = JSON.parse(current);
} catch {
return null;
}
}
if (!current || typeof current !== "object") return null;
current = current[key];
}
return current;
}
function firstId(value) {
if (!Array.isArray(value) || value.length === 0) return null;
const head = value[0];
return head && typeof head.I1 === "string" ? head.I1 : null;
}
function firstDomQuestionId(doc) {
const el = doc == null ? void 0 : doc.querySelector("#paperPreview .topic-cont[identifier]");
return (el == null ? void 0 : el.getAttribute("identifier")) ?? null;
}
function domQuestionCount(doc) {
if (!doc) return null;
return doc.querySelectorAll("#paperPreview .question-item").length;
}
const HOOK_FIELD_PATHS = {
items: ["Data", "Answer", "TestPaperData", "Data", "Items"],
itemsNested: ["Data", "Answer", "TestPaperData", "Data", "Data", "Items"],
sheet: ["Data", "Answer", "AnswerSheet", "ResultList"],
result: ["Data", "Answer", "AnswerResult", "Data", "Items"]
};
function hookFieldLengths(payload) {
const out = {};
for (const [name, path] of Object.entries(HOOK_FIELD_PATHS)) {
try {
const value = pathValueThroughEmbeddedJson(payload, [...path]);
out[name] = Array.isArray(value) ? value.length : value === null || value === void 0 ? "null" : typeof value;
} catch (error) {
out[name] = `throw:${error.code ?? "unknown"}`;
}
}
return out;
}
function domShape(doc) {
const first = {};
if (!doc) return { types: [], first };
const types = [
...new Set(
[...doc.querySelectorAll("#paperPreview .topic-cont[itemtype]")].map((el) => el.getAttribute("itemtype") ?? "").filter(Boolean)
)
].slice(0, 8);
const q = doc.querySelector("#paperPreview .question-item");
if (q) {
first.hasTopicCont = q.querySelector(".topic-cont") !== null;
first.hasIdentifier = q.querySelector(".topic-cont[identifier]") !== null;
first.hasStem = q.querySelector(".topic-cont > p.text") !== null;
first.optionCount = q.querySelectorAll("ul.options > li").length;
first.hasTopicAnswer = q.querySelector(".topic-answer") !== null;
}
return { types, first };
}
function probeJoin(payload, doc) {
const items = dig(payload, [
"Data",
"Answer",
"TestPaperData",
"Data",
"Items"
]);
const sheet = dig(payload, ["Data", "Answer", "AnswerSheet", "ResultList"]);
const result = dig(payload, [
"Data",
"Answer",
"AnswerResult",
"Data",
"Items"
]);
const itemId = firstId(items);
const sheetId = firstId(sheet);
const domId = firstDomQuestionId(doc);
const dom = domShape(doc);
const sheetIds = new Set(
(Array.isArray(sheet) ? sheet : []).map((row) => row == null ? void 0 : row.I1).filter((id) => typeof id === "string")
);
const count2 = (v) => Array.isArray(v) ? v.length : 0;
return {
items: count2(items),
sheet: count2(sheet),
result: count2(result),
sheetIdMatchesItem: itemId !== null && firstId(sheet) === itemId,
resultIdMatchesItem: itemId !== null && firstId(result) === itemId,
sheetIdMatchesResult: sheetId !== null && firstId(result) === sheetId,
domIdFoundInSheet: domId === null ? null : sheetIds.has(domId),
markStatusValues: [
...new Set(
(Array.isArray(result) ? result : []).map((row) => row == null ? void 0 : row.MarkStatus).filter((v) => typeof v === "number")
)
].sort((a, b) => a - b).slice(0, 8),
domQuestionCount: domQuestionCount(doc),
hookFieldLengths: hookFieldLengths(payload),
domItemTypes: dom.types,
domFirstQuestion: dom.first
};
}
function aopengCaptureStatus(doc) {
if (!applicable) return null;
const filledSlots = AOPENG_PAPER_SLOTS.filter(
(slot) => capture.read(slot) != null
);
const slotShapes = {};
const slotJoinProbe = {};
for (const slot of filledSlots) {
const payload = capture.read(slot);
slotShapes[slot] = describeJsonShape(payload);
slotJoinProbe[slot] = probeJoin(payload, doc);
}
return {
target: installTarget,
installed,
filledSlots,
slotShapes,
slotJoinProbe
};
}
function installAopengResponseCapture(hostname) {
if (hostname.trim().toLowerCase() !== AOPENG_CAPTURE_HOST) return false;
applicable = true;
let target;
try {
if (typeof unsafeWindow !== "undefined")
target = unsafeWindow == null ? void 0 : unsafeWindow.XMLHttpRequest;
} catch {
target = void 0;
}
if (target) installTarget = "page";
else if (typeof XMLHttpRequest !== "undefined") {
target = XMLHttpRequest;
installTarget = "sandbox";
}
if (!target) return false;
installed = capture.install(target);
return installed;
}
const PAGED_PATH = "/exam-ans/exam/test/reVersionTestStartNew";
const CHA0XING_EXAM_PREVIEW_PATH = "/exam-ans/mooc2/exam/preview";
const CHA0XING_EXAM_RESUME_KEY = "aiask_chaoxing_exam_resume_v1";
const RESUME_TTL_MS = 2 * 6e4;
const OPTION_ID_PATTERN = /^option-(0|[1-9][0-9]*)$/;
const TYPE_SELECTOR = 'input[name^="type"]:not(#type):not([name^="typeName"])';
const CHOICE_SELECTOR = ".stem_answer .answerBg";
const SELECTED_SELECTOR = ".check_answer, .check_answer_dx";
function isChaoxingHost(hostname) {
return hostname === "chaoxing.com" || hostname.endsWith(".chaoxing.com");
}
function parseResumeMarker(value) {
if (!value) return null;
try {
const marker = JSON.parse(value);
if (marker.v !== 1 || typeof marker.origin !== "string" || typeof marker.sourceHref !== "string" || typeof marker.createdAt !== "number" || typeof marker.expiresAt !== "number")
return null;
return marker;
} catch {
return null;
}
}
function storageValue(storage, key) {
try {
return storage.getItem(key);
} catch {
return null;
}
}
function shouldAutoResumeChaoxingExam(location2, storage, now = Date.now()) {
if (!isChaoxingHost(location2.hostname) || location2.pathname !== CHA0XING_EXAM_PREVIEW_PATH)
return false;
const marker = parseResumeMarker(
storageValue(storage, CHA0XING_EXAM_RESUME_KEY)
);
return marker !== null && marker.origin === location2.origin && marker.createdAt <= now + 3e4 && marker.expiresAt > now;
}
function clearChaoxingExamAutoResume(storage) {
try {
storage.removeItem(CHA0XING_EXAM_RESUME_KEY);
} catch {
}
}
function defaultDelay(ms, signal) {
if (signal.aborted) return Promise.resolve(false);
return new Promise((resolve) => {
const timer = setTimeout(() => {
signal.removeEventListener("abort", abort);
resolve(true);
}, ms);
const abort = () => {
clearTimeout(timer);
resolve(false);
};
signal.addEventListener("abort", abort, { once: true });
});
}
function normalizedText$1(element) {
return (element.textContent ?? "").replace(/\s+/g, "").trim();
}
function clickElement(element) {
const onclick = element.getAttribute("onclick") ?? "";
if (/finalSubmit\s*\(/i.test(onclick)) return false;
const clickable = element;
if (typeof clickable.click !== "function") return false;
clickable.click();
return true;
}
function selected(target) {
return target.querySelector(SELECTED_SELECTOR) !== null;
}
function choiceTargets(target) {
return Array.from(target.querySelectorAll(CHOICE_SELECTOR));
}
function desiredChoiceIndexes(plan, targetCount) {
if (plan.operations.length === 0 || plan.operations.some((operation) => operation.kind !== "choose"))
return null;
const indexes = plan.operations.map((operation) => {
if (operation.kind !== "choose") return -1;
const match = OPTION_ID_PATTERN.exec(operation.optionId);
return match ? Number(match[1]) : -1;
});
if (indexes.some((index) => index < 0 || index >= targetCount) || new Set(indexes).size !== indexes.length)
return null;
return indexes;
}
function questionType(target) {
const typeTarget = target.querySelector(TYPE_SELECTOR);
const view = target.ownerDocument.defaultView;
return view && typeTarget instanceof view.HTMLInputElement ? typeTarget.value : (typeTarget == null ? void 0 : typeTarget.getAttribute("value")) ?? "";
}
function isChaoxingExamPreviewReady(document2, resolveUeditorBodies2) {
const questions = Array.from(document2.querySelectorAll(".questionLi"));
if (questions.length === 0) return false;
for (const question of questions) {
if (!question.querySelector("h3.mark_name")) return false;
const type = questionType(question);
if (type === "0" || type === "1" || type === "3") {
const targets = question.querySelectorAll(CHOICE_SELECTOR);
const contents = question.querySelectorAll(
".stem_answer .answerBg .answer_p"
);
if (targets.length === 0 || targets.length !== contents.length)
return false;
continue;
}
if (type !== "2" && type !== "4") return false;
const textareas = Array.from(
question.querySelectorAll(
type === "2" ? 'textarea[name^="answerEditor"]' : 'textarea[id^="answer"][name^="answer"]:not([id^="answerEditor"])'
)
);
if (textareas.length === 0) return false;
let bodies;
try {
bodies = resolveUeditorBodies2(textareas);
} catch {
return false;
}
if (bodies.length !== textareas.length || bodies.some(
(body) => {
var _a2;
return !(body == null ? void 0 : body.isConnected) || ((_a2 = body.getAttribute("contenteditable")) == null ? void 0 : _a2.toLowerCase()) !== "true";
}
))
return false;
}
return true;
}
function safeSaveButtons(target) {
const candidates = Array.from(target.querySelectorAll(".saveButtonClass"));
if (candidates.length === 0) return null;
for (const button of candidates) {
if (!button.isConnected) return null;
const label = `${button.textContent ?? ""} ${button.getAttribute("value") ?? ""}`.replace(/\s+/g, "").trim();
const onclick = button.getAttribute("onclick") ?? "";
if (!label.includes("保存") || /交卷|提交试卷/.test(label)) return null;
if (/finalSubmit\s*\(/i.test(onclick)) return null;
}
return candidates;
}
class ChaoxingExamRuntime {
constructor(options = {}) {
__publicField(this, "questions", /* @__PURE__ */ new Map());
this.options = options;
}
beginCapture() {
this.questions.clear();
}
registerQuestion(registration) {
this.questions.set(registration.path, registration);
}
async prepareStart(ctx) {
var _a2, _b, _c;
if (ctx.signal.aborted || !isChaoxingHost(ctx.location.hostname) || ctx.location.pathname !== PAGED_PATH)
return "ready";
const previewLinks = Array.from(
ctx.document.querySelectorAll("a.completeBtn")
).filter(
(element) => element.isConnected && normalizedText$1(element) === "整卷预览" && /^\s*topreview\s*\(\s*\)\s*;?\s*$/.test(
element.getAttribute("onclick") ?? ""
)
);
if (previewLinks.length !== 1) return "ready";
const storage = (_a2 = ctx.document.defaultView) == null ? void 0 : _a2.sessionStorage;
if (!storage) return "ready";
const now = ((_c = (_b = this.options).now) == null ? void 0 : _c.call(_b)) ?? Date.now();
const marker = {
v: 1,
origin: ctx.location.origin,
sourceHref: ctx.location.href,
createdAt: now,
expiresAt: now + RESUME_TTL_MS
};
try {
storage.setItem(CHA0XING_EXAM_RESUME_KEY, JSON.stringify(marker));
if (!storageValue(storage, CHA0XING_EXAM_RESUME_KEY)) return "ready";
const previewLink = previewLinks[0];
if (!previewLink || !clickElement(previewLink)) {
clearChaoxingExamAutoResume(storage);
return "ready";
}
return "navigating";
} catch {
clearChaoxingExamAutoResume(storage);
return "ready";
}
}
preparePlan(plan, signal) {
if (signal.aborted) return false;
const question = this.questions.get(plan.path);
if (!(question == null ? void 0 : question.target.isConnected)) return false;
if (question.mode !== "preview") return true;
if (plan.operations.every((operation) => operation.kind === "write"))
return plan.operations.length > 0 && (questionType(question.target) === "2" || questionType(question.target) === "4");
const targets = choiceTargets(question.target);
const desired = desiredChoiceIndexes(plan, targets.length);
const type = questionType(question.target);
if (!desired || type !== "0" && type !== "1" && type !== "3") return false;
if (type === "0" || type === "3") return desired.length === 1;
const desiredSet = new Set(desired);
for (const [index, target] of targets.entries()) {
if (selected(target) && !desiredSet.has(index) && !clickElement(target))
return false;
}
return targets.every(
(target, index) => !selected(target) || desiredSet.has(index)
);
}
async commitPlan(plan, signal) {
if (signal.aborted) return false;
const question = this.questions.get(plan.path);
if (!(question == null ? void 0 : question.target.isConnected)) return false;
if (question.mode !== "preview") return true;
if (plan.operations.every((operation) => operation.kind === "write")) {
const type2 = questionType(question.target);
if (plan.operations.length === 0 || type2 !== "2" && type2 !== "4")
return false;
const saves = safeSaveButtons(question.target);
const expectedSaves = type2 === "4" ? 1 : plan.operations.length;
if (!saves || saves.length !== expectedSaves) return false;
for (const save of saves) if (!clickElement(save)) return false;
return (this.options.delay ?? defaultDelay)(250, signal);
}
const targets = choiceTargets(question.target);
const desired = desiredChoiceIndexes(plan, targets.length);
const type = questionType(question.target);
if (!desired || type !== "0" && type !== "1" && type !== "3") return false;
const desiredSet = new Set(desired);
if (!targets.every(
(target, index) => selected(target) === desiredSet.has(index)
))
return false;
return (this.options.delay ?? defaultDelay)(
type === "1" ? 600 : 250,
signal
);
}
dispose() {
this.questions.clear();
}
}
const CHA0XING_PACKAGE_IDS = Object.freeze({
studentstudy: "chaoxing-studentstudy",
examStudent: "chaoxing-exam-student",
newChapter: "chaoxing-new-chapter",
oldChapter: "chaoxing-old-chapter",
oldHomework: "chaoxing-old-homework",
dowork: "chaoxing-dowork"
});
const CHA0XING_ANSWERABLE_PATH = /work\/(doHomeWork|dowork|view)|studentstudy|exam|test\//iu;
const CHA0XING_UNROUTED_PACKAGE_ID = "chaoxing-unrouted";
const CHA0XING_STUDENTSTUDY_PATHS = [
"/mycourse/studentstudy",
"/mooc-ans/mycourse/studentstudy"
];
const REGEX_WORKER_SOURCE = [
"'use strict';",
"self.addEventListener('message', function (event) {",
" var request = event.data;",
" try {",
" var regex = new RegExp(request.pattern, request.flags || '');",
" var value;",
" if (request.kind === 'test') value = regex.test(request.value);",
" else if (request.kind === 'replace') value = request.value.replace(regex, request.replacement || '');",
" else {",
" var match = regex.exec(request.value);",
" value = match ? Array.from(match, function (part) { return part == null ? null : part; }) : null;",
" }",
" self.postMessage({ ok: true, value: value });",
" } catch (error) {",
" self.postMessage({ ok: false, code: 'regex_error', error: error instanceof Error ? error.message : 'regex failed' });",
" }",
"});"
].join("\n");
function createBrowserRegexWorker() {
const url = URL.createObjectURL(
new Blob([REGEX_WORKER_SOURCE], { type: "text/javascript" })
);
try {
return new Worker(url);
} finally {
URL.revokeObjectURL(url);
}
}
function createRuleExpressionServices(createWorker = createBrowserRegexWorker) {
const regex = new IsolatedRegexExecutor(createWorker);
const services = {
regex: (request) => regex.execute(request),
// ponytail: 同步求值即可——executeJsonPath 自带节点/结果预算,单线程下异步包装也拦不住长查询。
jsonPath: ({ value, query, signal }) => executeJsonPath(value, query, { signal })
};
return Object.freeze(services);
}
const RULE_EXPRESSION_SERVICES = createRuleExpressionServices();
const RULE_ENGINE_VERSION = "1.6.0";
const RULE_LIMITS = Object.freeze({
// 50_000 是协议硬顶(RULE_HARD_LIMITS)。抬到顶是被奥鹏按 id 连接的 O(n²) 逼的:
// 每题各扫一遍 sheet/result/items,50 题的正考在 10_000 步下必然 budget_exceeded、
// 静默收 0 题(#88)。wallMs 仍是真正的安全网,步数放宽不改变最坏耗时。
// ponytail: 天花板在 50~60 题之间。根治要把连接改成 O(n)(遍历接口数组 + format 拼
// #qid 选择器直查 DOM),不是继续抬预算——协议顶已经到了。
maxSteps: 5e4,
// 2_000 → 6_000(协议硬顶 10_000)。**这条是被 2026-08-21 真页逼出来的,不是预防性放宽**:
// 一页三份章测共 114 道题,2 秒下 5 次里 3 次 `timeout`——首次成功过一回、诊断复跑就
// 抓 0 题,纯抛硬币。墙钟超时抛 RuleExecutionError,逐题 try/catch 只接 RuleDomainError
// 接不住,症状是整页 0 题,和「规则没适配」长得一模一样,离线永远测不出来。
// 天花板抬了不等于人人都用:一页一卷的那三个包在自己的 limits 里照旧写 2_000。
maxWallMs: 6e3,
maxAsyncMs: 1e3,
maxLoopIterations: 256,
maxCallDepth: 8,
// 收录一道选择题约占 16 个 DOM 引用(题干/答案/选项各一层),1_000 只够 62 题,
// 而奥鹏一场正考就是 50 题。超限抛的是 RuleExecutionError,逐题 `try/catch` 只接
// RuleDomainError(interpreter.ts)接不住,症状是整页 0 题而非跳过一题——离崖太近。
maxDomRefs: 4e3
});
const CORE_RULE_PRIMITIVES = Object.freeze([
// 页面与 DOM 读取
"page.location",
"page.queryParam",
"dom.queryCss",
"dom.queryCssAll",
"dom.queryXPath",
"dom.queryXPathAll",
"dom.text",
"dom.content",
"dom.attr",
"dom.property",
"dom.closest",
"dom.parent",
"dom.children",
"dom.index",
"wait.selector",
// 同源子帧
"frame.list",
"frame.enter",
"frame.findSameOrigin",
"frame.findAllSameOrigin",
// 文本与集合归一
"content.sanitize",
"text.includes",
"text.stripOptionPrefix",
"text.normalizeTruth",
"question.normalizeLeafType",
"array.append",
// 抓题与收录
"capture.registerLeafDom",
"capture.registerLeafBindingDom",
"capture.registerTree",
"capture.harvestLeaf",
"capture.finish",
// 安全闸内写入(全部需要 SafetyGate 票据)
"answer.applyPlan",
"dom.clickAnswer",
"dom.setChecked",
"dom.setValue",
"dom.setSelected",
"matching.pair",
// 生命周期
"observe.mutation",
"observe.urlChange"
]);
const CORE_CAPABILITIES = Object.freeze([
"dom-read",
"frame-read",
"runtime-read",
"answer-write"
]);
const GENERIC_DOM_RULE_POLICY = {
primitives: new Set(CORE_RULE_PRIMITIVES),
capabilities: new Set(CORE_CAPABILITIES),
limits: RULE_LIMITS
};
const AOPENG_RULE_POLICY = {
primitives: /* @__PURE__ */ new Set([...CORE_RULE_PRIMITIVES, "aopeng.paperData"]),
capabilities: /* @__PURE__ */ new Set([...CORE_CAPABILITIES, "network-read"]),
limits: RULE_LIMITS
};
const CHA0XING_RULE_POLICY = {
primitives: /* @__PURE__ */ new Set([
...CORE_RULE_PRIMITIVES,
"chaoxing.normalizeTitle",
"chaoxing.decodeFont",
"chaoxing.harvestAnswerValues",
"chaoxing.ueditorBodies",
"chaoxing.examRegisterQuestion",
"chaoxing.examPreparePlan",
"chaoxing.examCommitPlan",
"chaoxing.doworkCommitPlan",
"chaoxing.studentstudyCommitPlan",
"chaoxing.oldHomeworkCommitPlan",
"chaoxing.oldChapterCommitPlan",
"chaoxing.newChapterCommitPlan"
]),
capabilities: new Set(CORE_CAPABILITIES),
limits: RULE_LIMITS
};
const TRUSTED_REMOTE_RULE_PLATFORMS = Object.freeze([
Object.freeze({
platform: "wangxiao",
packageId: "wangxiao-xatu-chapter-assessment",
hosts: Object.freeze(["xatu.168wangxiao.com"]),
policy: GENERIC_DOM_RULE_POLICY
}),
Object.freeze({
platform: "aopeng",
packageId: "aopeng-os-homework-online",
hosts: Object.freeze(["os.open.com.cn"]),
policy: AOPENG_RULE_POLICY
})
]);
const SUPPORTED_HOST_PATTERN = /^(?:(?:[^.]+\.)*chaoxing\.com|xatu\.168wangxiao\.com|os\.open\.com\.cn)$/u;
const normalizedHost = (hostname) => hostname.trim().toLocaleLowerCase();
function trustedRemoteRulePlatformFor(hostname) {
const host = normalizedHost(hostname);
return TRUSTED_REMOTE_RULE_PLATFORMS.find(
(entry) => entry.hosts.some((candidate) => candidate === host)
) ?? null;
}
function trustedRemoteRulePlatformByPackageId(packageId) {
return TRUSTED_REMOTE_RULE_PLATFORMS.find(
(entry) => entry.packageId === packageId
) ?? null;
}
function objectValue(value) {
return value !== null && typeof value === "object";
}
function elementValue(value) {
if (!objectValue(value) || typeof value.tagName !== "string" || typeof value.getAttribute !== "function")
return null;
return value;
}
function ueditorApi(document2) {
var _a2, _b;
let candidate;
try {
candidate = (_a2 = pageWindowForDocument(document2)) == null ? void 0 : _a2.UE;
} catch {
candidate = null;
}
if (!candidate) {
try {
if (typeof unsafeWindow !== "undefined") candidate = unsafeWindow == null ? void 0 : unsafeWindow.UE;
} catch {
candidate = null;
}
}
if (!candidate) {
candidate = (_b = document2.defaultView) == null ? void 0 : _b.UE;
}
return objectValue(candidate) ? candidate : null;
}
function bodyFromEditor(value) {
return objectValue(value) ? elementValue(value.body) : null;
}
function expectedEditorFrame(textarea) {
const host = textarea.closest(".subEditor") ?? textarea.parentElement;
return (host == null ? void 0 : host.querySelector(
'iframe[id^="ueditor_"]'
)) ?? null;
}
function validEditorBody(body, expectedFrame) {
var _a2, _b;
if (!body || !body.isConnected || ((_a2 = body.getAttribute("contenteditable")) == null ? void 0 : _a2.toLowerCase()) !== "true")
return false;
try {
const frame = (_b = body.ownerDocument.defaultView) == null ? void 0 : _b.frameElement;
return frame === expectedFrame;
} catch {
return false;
}
}
function resolveUeditorBody(textarea, api) {
const editor = resolveUeditorEditor(textarea, api);
return editor ? bodyFromEditor(editor) : null;
}
function resolveUeditorEditor(textarea, api) {
var _a2;
if (!textarea.id) return null;
const expectedFrame = expectedEditorFrame(textarea);
if (!(expectedFrame == null ? void 0 : expectedFrame.isConnected)) return null;
if (objectValue(api.instants)) {
for (const editor of Object.values(api.instants)) {
const body = bodyFromEditor(editor);
if (!validEditorBody(body, expectedFrame)) continue;
const candidate = editor;
const container = elementValue(candidate.container);
if (candidate.id === textarea.id || (container == null ? void 0 : container.contains(textarea)) || ((_a2 = body.ownerDocument.defaultView) == null ? void 0 : _a2.frameElement) === expectedFrame)
return candidate;
}
}
if (typeof api.getEditor !== "function") return null;
try {
const editor = api.getEditor.call(api, textarea.id);
const body = bodyFromEditor(editor);
return validEditorBody(body, expectedFrame) ? editor : null;
} catch {
return null;
}
}
function resolveUeditorBodies(targets, document2) {
const fallbackApi = ueditorApi(document2);
const apis = /* @__PURE__ */ new Map();
return targets.map((target) => {
const ownerDocument = target.ownerDocument;
if (!apis.has(ownerDocument))
apis.set(ownerDocument, ueditorApi(ownerDocument) ?? fallbackApi);
const api = apis.get(ownerDocument);
return api ? resolveUeditorBody(target, api) : null;
});
}
function validatedRulePackageIdFor(location2) {
const page = new URL(location2.href);
if (page.hostname !== "chaoxing.com" && !page.hostname.endsWith(".chaoxing.com"))
return null;
if (CHA0XING_STUDENTSTUDY_PATHS.some(
(pathname) => pathname === page.pathname
) && page.searchParams.get("mooc2") === "1")
return CHA0XING_PACKAGE_IDS.studentstudy;
if (page.pathname === "/mooc-ans/work/selectWorkQuestionYiPiYue" || page.pathname === "/work/selectWorkQuestionYiPiYue")
return CHA0XING_PACKAGE_IDS.studentstudy;
if (page.pathname === "/exam-ans/exam/test/reVersionTestStartNew" || page.pathname === CHA0XING_EXAM_PREVIEW_PATH)
return CHA0XING_PACKAGE_IDS.examStudent;
if (page.pathname === "/mooc-ans/work/doHomeWorkNew" && page.searchParams.get("mooc2") === "1")
return CHA0XING_PACKAGE_IDS.newChapter;
if (page.pathname === "/mooc-ans/work/doHomeWorkNew" && page.searchParams.get("mooc2") === "0")
return CHA0XING_PACKAGE_IDS.oldChapter;
if (page.pathname === "/mooc-ans/work/doHomeWorkNew" && page.searchParams.get("mooc") === "1")
return CHA0XING_PACKAGE_IDS.oldHomework;
if (page.pathname === "/mooc-ans/mooc2/work/dowork")
return CHA0XING_PACKAGE_IDS.dowork;
return null;
}
function isNewCourseStudyUrl(location2) {
const page = new URL(location2.href);
if (page.hostname !== "chaoxing.com" && !page.hostname.endsWith(".chaoxing.com"))
return false;
return CHA0XING_STUDENTSTUDY_PATHS.some(
(pathname) => pathname === page.pathname
) && page.searchParams.get("mooc2") === "1";
}
function legacyStudentstudyUpgradeUrl(location2) {
const page = new URL(location2.href);
if (page.hostname !== "chaoxing.com" && !page.hostname.endsWith(".chaoxing.com"))
return null;
if (!CHA0XING_STUDENTSTUDY_PATHS.some((pathname) => pathname === page.pathname))
return null;
if (page.searchParams.get("mooc2") === "1") return null;
page.pathname = "/mycourse/studentstudy";
page.searchParams.set("mooc2", "1");
return page.toString();
}
const DOWORK_SAVE_TEXT = "暂时保存";
const DOWORK_SAVE_HANDLERS = /* @__PURE__ */ new Set(["saveWork()", "saveWork();"]);
const NO_SUBMIT_SAVE_TEXT = "暂时保存";
const NO_SUBMIT_SAVE_HANDLERS = /* @__PURE__ */ new Set(["noSubmit()", "noSubmit();"]);
const NO_SUBMIT_SAVE_CLASSES = /* @__PURE__ */ new Set(["btnSave", "btnGray_1"]);
const NO_SUBMIT_BLOCKED_CLASSES = /* @__PURE__ */ new Set([
"btnSubmit",
"Btn_blue_1",
"completeBtn"
]);
const NO_SUBMIT_MAX_FRAME_DEPTH = 6;
const NO_SUBMIT_MAX_FRAMES = 64;
const NO_SUBMIT_PENDING_TEXT = "正在暂存...";
const NO_SUBMIT_SUCCESS_TEXT = "保存成功";
const NO_SUBMIT_CONFIRM_TIMEOUT_MS = 1e4;
const STUDENTSTUDY_READY_TIMEOUT_MS = 5e3;
const PAGE_WINDOW_PROBE_ATTRIBUTE = "data-aiask-page-window-probe";
let pageWindowProbeSequence = 0;
function normalizedElementText(element) {
return (element.textContent ?? "").replace(/\s+/g, " ").trim();
}
function hasAnyClass(element, classes) {
return [...classes].some((className) => element.classList.contains(className));
}
function safeDoworkSaveTarget(document2) {
var _a2;
const candidates = [...document2.querySelectorAll("a")].filter((element) => {
const onclick = element.getAttribute("onclick") ?? "";
return normalizedElementText(element) === DOWORK_SAVE_TEXT || onclick.includes("saveWork");
});
if (candidates.length !== 1) return null;
const target = candidates[0];
if (!target || !target.isConnected || target.tagName.toLowerCase() !== "a" || normalizedElementText(target) !== DOWORK_SAVE_TEXT || !DOWORK_SAVE_HANDLERS.has(((_a2 = target.getAttribute("onclick")) == null ? void 0 : _a2.trim()) ?? "") || target.classList.contains("completeBtn") || target.closest(".completeBtn"))
return null;
return target;
}
function sameOriginDocuments(root) {
const queue = [
{ document: root, depth: 0 }
];
const seen = /* @__PURE__ */ new Set();
const documents = [];
let frameCount = 0;
while (queue.length > 0) {
const current = queue.shift();
if (!current || seen.has(current.document)) continue;
seen.add(current.document);
documents.push(current.document);
if (current.depth >= NO_SUBMIT_MAX_FRAME_DEPTH) continue;
let frames;
try {
frames = [...current.document.querySelectorAll("iframe")];
} catch {
return null;
}
for (const frame of frames) {
frameCount += 1;
if (frameCount > NO_SUBMIT_MAX_FRAMES) return null;
let child = null;
try {
child = frame.contentDocument;
} catch {
child = null;
}
if (child && !seen.has(child))
queue.push({ document: child, depth: current.depth + 1 });
}
}
return documents;
}
function isStudentstudyTextReady(document2) {
const documents = sameOriginDocuments(document2);
if (!documents) return false;
const questions = documents.flatMap((current) => [
...current.querySelectorAll(".TiMu")
]);
if (questions.length === 0) return false;
for (const question of questions) {
const typeTarget = question.querySelector('input[name^="answertype"]');
const view = typeTarget == null ? void 0 : typeTarget.ownerDocument.defaultView;
const type = view && typeTarget instanceof view.HTMLInputElement ? typeTarget.value : (typeTarget == null ? void 0 : typeTarget.getAttribute("value")) ?? "";
if (type !== "2") continue;
const textareas = [
...question.querySelectorAll(
'textarea[name^="answerEditor"]'
)
];
if (textareas.length === 0) return false;
let bodies;
try {
bodies = resolveUeditorBodies(textareas, document2);
} catch {
return false;
}
if (bodies.length !== textareas.length || bodies.some(
(body) => {
var _a2;
return !(body == null ? void 0 : body.isConnected) || ((_a2 = body.getAttribute("contenteditable")) == null ? void 0 : _a2.toLowerCase()) !== "true";
}
))
return false;
}
return true;
}
function validNoSubmitTarget(target) {
var _a2;
return !(!target || !target.isConnected || target.tagName.toLowerCase() !== "a" || normalizedElementText(target) !== NO_SUBMIT_SAVE_TEXT || !hasAnyClass(target, NO_SUBMIT_SAVE_CLASSES) || !target.classList.contains("workBtnIndex") || !NO_SUBMIT_SAVE_HANDLERS.has(
((_a2 = target.getAttribute("onclick")) == null ? void 0 : _a2.trim()) ?? ""
) || hasAnyClass(target, NO_SUBMIT_BLOCKED_CLASSES) || target.closest(".btnSubmit, .Btn_blue_1, .completeBtn"));
}
const noSubmitCandidatesIn = (current) => {
try {
return [...current.querySelectorAll("a")].filter((element) => {
const onclick = element.getAttribute("onclick") ?? "";
return normalizedElementText(element) === NO_SUBMIT_SAVE_TEXT || onclick.includes("noSubmit");
});
} catch {
return null;
}
};
function safeNoSubmitSaveTargets(document2) {
const documents = sameOriginDocuments(document2);
if (!documents) return null;
const targets = [];
for (const current of documents) {
let answerable = false;
try {
answerable = current.querySelector(ANSWERABLE_QUESTION_SELECTOR) !== null;
} catch {
return null;
}
if (!answerable) continue;
const candidates = noSubmitCandidatesIn(current);
if (!candidates) return null;
if (candidates.length === 0) continue;
if (candidates.length > 1) return null;
if (!validNoSubmitTarget(candidates[0])) return null;
targets.push(candidates[0]);
}
return targets.length > 0 ? targets : null;
}
const ANSWERABLE_QUESTION_SELECTOR = '.TiMu input[name^="answertype"]';
const doworkSaveTargets = (document2) => {
const target = safeDoworkSaveTarget(document2);
return target ? [target] : null;
};
function unsafePageWindow() {
try {
return typeof unsafeWindow === "undefined" ? null : unsafeWindow ?? null;
} catch {
return null;
}
}
function pageWindowForDocument(document2) {
var _a2;
const root = unsafePageWindow();
if (!root) return document2.defaultView;
const documentElement = document2.documentElement;
if (!documentElement) return null;
const previousProbe = documentElement.getAttribute(
PAGE_WINDOW_PROBE_ATTRIBUTE
);
const probe = `aiask-${++pageWindowProbeSequence}`;
documentElement.setAttribute(PAGE_WINDOW_PROBE_ATTRIBUTE, probe);
const queue = [
{ window: root, depth: 0 }
];
const seen = /* @__PURE__ */ new Set();
let frameCount = 0;
try {
while (queue.length > 0) {
const current = queue.shift();
if (!current || seen.has(current.window)) continue;
seen.add(current.window);
try {
if (((_a2 = current.window.document.documentElement) == null ? void 0 : _a2.getAttribute(
PAGE_WINDOW_PROBE_ATTRIBUTE
)) === probe)
return current.window;
} catch {
continue;
}
if (current.depth >= NO_SUBMIT_MAX_FRAME_DEPTH) continue;
let length = 0;
try {
length = current.window.frames.length;
} catch {
continue;
}
for (let index = 0; index < length; index += 1) {
frameCount += 1;
if (frameCount > NO_SUBMIT_MAX_FRAMES) return null;
try {
const child = current.window.frames[index];
if (!seen.has(child))
queue.push({ window: child, depth: current.depth + 1 });
} catch {
}
}
}
return null;
} finally {
if (previousProbe === null)
documentElement.removeAttribute(PAGE_WINDOW_PROBE_ATTRIBUTE);
else
documentElement.setAttribute(PAGE_WINDOW_PROBE_ATTRIBUTE, previousProbe);
}
}
function pageWindowsInFrameTree() {
const root = unsafePageWindow();
if (!root) return [];
const roots = [root];
try {
const top = root.top;
if (top && top !== root) roots.unshift(top);
} catch {
}
const out = [];
const seen = /* @__PURE__ */ new Set();
const queue = roots.map((window2) => ({ window: window2, depth: 0 }));
let frameCount = 0;
while (queue.length > 0) {
const current = queue.shift();
if (!current || seen.has(current.window)) continue;
seen.add(current.window);
out.push(current.window);
if (current.depth >= NO_SUBMIT_MAX_FRAME_DEPTH) continue;
let length = 0;
try {
length = current.window.frames.length;
} catch {
continue;
}
for (let index = 0; index < length; index += 1) {
frameCount += 1;
if (frameCount > NO_SUBMIT_MAX_FRAMES) return out;
try {
const child = current.window.frames[index];
if (!seen.has(child))
queue.push({ window: child, depth: current.depth + 1 });
} catch {
}
}
}
return out;
}
function runNoSubmitSave(target, signal, requiresTextSync) {
if (signal.aborted || !target.isConnected) return Promise.resolve(false);
const pageWindow = pageWindowForDocument(target.ownerDocument);
const handler = pageWindow == null ? void 0 : pageWindow.noSubmit;
if (!pageWindow || typeof handler !== "function")
return Promise.resolve(false);
if (requiresTextSync && !syncUeditorAnswers(target.ownerDocument, pageWindow))
return Promise.resolve(false);
return new Promise((resolve) => {
const originalAlert = pageWindow.alert;
let settled = false;
let started = false;
let successSeen = false;
const finish = (value) => {
if (settled) return;
settled = true;
globalThis.clearTimeout(timeout);
signal.removeEventListener("abort", onAbort);
pageWindow.removeEventListener("pagehide", onPageHide);
pageWindow.removeEventListener("beforeunload", onPageHide);
try {
pageWindow.alert = originalAlert;
} catch {
}
resolve(value);
};
const onAbort = () => finish(false);
const onPageHide = () => finish(started);
const interceptedAlert = (message) => {
const text = String(message ?? "").replace(/\s+/g, "").trim();
if (text === NO_SUBMIT_SUCCESS_TEXT) {
successSeen = true;
if (started) finish(true);
return;
}
finish(false);
originalAlert.call(pageWindow, String(message ?? ""));
};
const timeout = globalThis.setTimeout(
() => finish(false),
NO_SUBMIT_CONFIRM_TIMEOUT_MS
);
signal.addEventListener("abort", onAbort, { once: true });
pageWindow.addEventListener("pagehide", onPageHide, { once: true });
pageWindow.addEventListener("beforeunload", onPageHide, { once: true });
try {
pageWindow.alert = interceptedAlert;
handler.call(pageWindow);
started = normalizedElementText(target) === NO_SUBMIT_PENDING_TEXT;
if (!started) finish(false);
else if (successSeen) finish(true);
} catch {
finish(false);
}
});
}
function syncUeditorAnswers(document2, pageWindow) {
var _a2;
const targets = [
...document2.querySelectorAll(
'textarea[name^="answerEditor"], textarea[name^="answer"]:not([name^="answerEditor"])'
)
];
if (targets.length === 0) return true;
const api = objectValue(pageWindow.UE) ? pageWindow.UE : ueditorApi(document2);
for (const textarea of targets) {
const editor = api ? resolveUeditorEditor(textarea, api) : null;
const body = editor ? bodyFromEditor(editor) : null;
const text = ((_a2 = body == null ? void 0 : body.textContent) == null ? void 0 : _a2.trim()) ?? "";
if (!text) continue;
if (!api || !editor || typeof editor.sync !== "function") return false;
try {
editor.sync.call(editor);
} catch {
return false;
}
if (!textarea.value.trim()) return false;
}
return true;
}
class ChaoxingDeferredSaveRuntime {
constructor(safeTargets, execute = (target) => {
target.click();
return true;
}) {
__publicField(this, "document", null);
__publicField(this, "pending", false);
__publicField(this, "requiresTextSync", false);
this.safeTargets = safeTargets;
this.execute = execute;
}
stagePlan(plan, document2, signal) {
if (signal.aborted || plan.operations.length === 0 || !this.safeTargets(document2))
return false;
this.document = document2;
this.pending = true;
if (plan.operations.some((operation) => operation.kind === "write"))
this.requiresTextSync = true;
return true;
}
/**
* 逐份卷子各暂存一次。**任一份失败即整体失败**——报「存好了」而其实只存了一半,
* 比报失败糟得多(坑 31 同一条:不把自己发出的动作当成事实)。
*/
async persist(ctx) {
if (!this.pending) return true;
if (ctx.signal.aborted || ctx.document !== this.document) return false;
const targets = this.safeTargets(ctx.document);
if (!targets) return false;
try {
for (const target of targets)
if (!await this.execute(target, ctx.signal, this.requiresTextSync))
return false;
this.pending = false;
this.document = null;
this.requiresTextSync = false;
return true;
} catch {
return false;
}
}
dispose() {
this.document = null;
this.pending = false;
this.requiresTextSync = false;
}
}
function chaoxingRuleOptions(packageId, store, services, configureRegistry) {
return {
platform: "chaoxing",
packageId,
hosts: ["chaoxing.com"],
store,
policy: CHA0XING_RULE_POLICY,
services,
configureRegistry
};
}
const CHA0XING_SAVE_VARIANTS = Object.freeze({
[CHA0XING_PACKAGE_IDS.dowork]: {
commitKey: "commitDoworkPlan",
safeTargets: doworkSaveTargets
},
[CHA0XING_PACKAGE_IDS.studentstudy]: {
commitKey: "commitStudentstudyPlan",
safeTargets: safeNoSubmitSaveTargets,
execute: runNoSubmitSave,
ready: isStudentstudyTextReady
},
[CHA0XING_PACKAGE_IDS.oldHomework]: {
commitKey: "commitOldHomeworkPlan",
safeTargets: safeNoSubmitSaveTargets,
execute: runNoSubmitSave
},
[CHA0XING_PACKAGE_IDS.newChapter]: {
commitKey: "commitNewChapterPlan",
safeTargets: safeNoSubmitSaveTargets,
execute: runNoSubmitSave
},
[CHA0XING_PACKAGE_IDS.oldChapter]: {
commitKey: "commitOldChapterPlan",
safeTargets: safeNoSubmitSaveTargets,
execute: runNoSubmitSave
}
});
class ChaoxingSaveRuleAdapter extends JsonRulePlatformAdapter {
constructor(variant, packageId, store, typr, table, services) {
const saveRuntime = new ChaoxingDeferredSaveRuntime(
variant.safeTargets,
variant.execute
);
super(
chaoxingRuleOptions(
packageId,
store,
services,
(registry, environment) => registerChaoxingRuleHooks(registry, {
typr,
table,
refs: environment.refs,
resolveUeditorBodies: (targets) => resolveUeditorBodies(targets, environment.ctx.document),
[variant.commitKey]: (plan, signal) => saveRuntime.stagePlan(plan, environment.ctx.document, signal)
})
)
);
__publicField(this, "saveRuntime");
__publicField(this, "ready");
this.saveRuntime = saveRuntime;
this.ready = variant.ready;
}
async captureTrees(ctx) {
const ready = this.ready;
if (ready) {
await waitUntil(() => ready(ctx.document), {
timeout: STUDENTSTUDY_READY_TIMEOUT_MS,
interval: 50,
signal: ctx.signal
});
if (ctx.signal.aborted) return [];
}
return super.captureTrees(ctx);
}
persistAnswers(ctx) {
return this.saveRuntime.persist(ctx);
}
async dispose() {
this.saveRuntime.dispose();
await super.dispose();
}
}
class ChaoxingExamRuleAdapter extends JsonRulePlatformAdapter {
constructor(packageId, store, typr, table, services) {
const examRuntime = new ChaoxingExamRuntime();
super(
chaoxingRuleOptions(
packageId,
store,
services,
(registry, environment) => registerChaoxingRuleHooks(registry, {
typr,
table,
refs: environment.refs,
resolveUeditorBodies: (targets) => resolveUeditorBodies(targets, environment.ctx.document),
registerExamQuestion: (registration) => examRuntime.registerQuestion(registration),
prepareExamPlan: (plan, signal) => examRuntime.preparePlan(plan, signal),
commitExamPlan: (plan, signal) => examRuntime.commitPlan(plan, signal)
})
)
);
__publicField(this, "examRuntime");
this.examRuntime = examRuntime;
}
async captureTrees(ctx) {
this.examRuntime.beginCapture();
if (ctx.location.pathname === CHA0XING_EXAM_PREVIEW_PATH && !await waitUntil(
() => isChaoxingExamPreviewReady(
ctx.document,
(targets) => resolveUeditorBodies(targets, ctx.document)
),
{ timeout: 5e3, interval: 50, signal: ctx.signal }
))
return [];
return super.captureTrees(ctx);
}
prepareStart(ctx) {
return this.examRuntime.prepareStart(ctx);
}
async dispose() {
this.examRuntime.dispose();
await super.dispose();
}
}
function createChaoxingRuleAdapter(packageId, store, typr, table, services) {
const args = [packageId, store, typr, table, services];
if (packageId === CHA0XING_PACKAGE_IDS.examStudent)
return new ChaoxingExamRuleAdapter(...args);
const saveVariant = CHA0XING_SAVE_VARIANTS[packageId];
if (saveVariant) return new ChaoxingSaveRuleAdapter(saveVariant, ...args);
return new JsonRulePlatformAdapter(
chaoxingRuleOptions(
packageId,
store,
services,
(registry, environment) => registerChaoxingRuleHooks(registry, {
typr,
table,
refs: environment.refs,
resolveUeditorBodies: (targets) => resolveUeditorBodies(targets, environment.ctx.document)
})
)
);
}
function createDefaultAdapterFactories(location2, typr, store, table = {}, services = RULE_EXPRESSION_SERVICES) {
const trustedRemote = trustedRemoteRulePlatformFor(
location2.hostname || new URL(location2.href).hostname
);
if (trustedRemote)
return [
() => new JsonRulePlatformAdapter({
platform: trustedRemote.platform,
packageId: trustedRemote.packageId,
hosts: trustedRemote.hosts,
store,
policy: trustedRemote.policy,
services,
// hook 只按平台注册;policy 里没放行的平台即使注册了也调不到(双闸)。
...trustedRemote.platform === "aopeng" ? {
configureRegistry: (registry) => registerAopengRuleHooks(registry, {
readCapturedResponse: readAopengCapturedResponse
})
} : {}
})
];
const packageId = validatedRulePackageIdFor(location2);
return packageId ? [() => createChaoxingRuleAdapter(packageId, store, typr, table, services)] : [];
}
const TIMED_READ_ROUNDS = 3;
const TIMED_READ_SLACK_SECONDS = 3;
const TIMED_READ_FALLBACK_SECONDS = 60;
const MAX_DATA_HOPS = 3;
const siteStateOf = (attachment) => attachment.job ? "job" : attachment.isPassed ? "passed" : "not-job";
const courseWindow = (document2) => {
try {
return pageWindowForDocument(document2) ?? null;
} catch {
return null;
}
};
function courseAttachments(documents) {
var _a2;
for (const document2 of documents) {
const list = (_a2 = courseWindow(document2)) == null ? void 0 : _a2.attachments;
if (Array.isArray(list)) return list;
}
return null;
}
const attachmentJobId = (attachment) => {
var _a2;
const raw = attachment.jobid || ((_a2 = attachment.property) == null ? void 0 : _a2._jobid);
return raw === void 0 || raw === null ? "" : String(raw);
};
const attachmentName = (attachment) => {
const property = attachment == null ? void 0 : attachment.property;
if (!property) return "";
const { name, title, bookname, author } = property;
if (typeof name === "string" && name) return name;
if (typeof title === "string" && title) return title;
if (typeof bookname === "string" && bookname)
return typeof author === "string" && author ? `${bookname} ${author}` : bookname;
return "";
};
const frameJobId = (document2) => {
var _a2, _b, _c;
let frame = ((_a2 = document2.defaultView) == null ? void 0 : _a2.frameElement) ?? null;
for (let hop = 0; frame && hop < MAX_DATA_HOPS; hop += 1) {
const raw = frame.getAttribute("data");
if (raw) {
try {
const parsed = JSON.parse(raw);
const id = parsed.jobid || parsed._jobid;
if (id !== void 0 && id !== null && String(id)) return String(id);
} catch {
}
}
try {
frame = ((_c = (_b = frame.ownerDocument) == null ? void 0 : _b.defaultView) == null ? void 0 : _c.frameElement) ?? null;
} catch {
return null;
}
}
return null;
};
const MAX_MARKER_HOPS = 12;
const flattened$1 = (value) => (value ?? "").replace(/\s+/gu, "");
const carriesDoneMarker = (root) => {
const text = courseConfig().taskDoneText;
if (flattened$1(root.textContent).includes(text)) return true;
for (const element of root.querySelectorAll("[aria-label], [title], [alt]"))
for (const attr of ["aria-label", "title", "alt"])
if (flattened$1(element.getAttribute(attr)).includes(text)) return true;
return false;
};
const frameElementOf = (document2) => {
var _a2;
try {
return ((_a2 = document2 == null ? void 0 : document2.defaultView) == null ? void 0 : _a2.frameElement) ?? null;
} catch {
return null;
}
};
const frameMarkedDone = (document2) => {
let node = frameElementOf(document2);
for (let hop = 0; node && hop < MAX_MARKER_HOPS; hop += 1) {
const parent = node.parentElement;
if (!parent) {
node = frameElementOf(node.ownerDocument);
continue;
}
if (parent.querySelectorAll("iframe, frame").length > 1) return false;
if (carriesDoneMarker(parent)) return true;
node = parent;
}
return false;
};
function taskKindOf(document2) {
for (const [kind, selector] of courseConfig().probes) {
let hit = null;
try {
hit = document2.querySelector(selector);
} catch {
continue;
}
if (hit) return kind;
}
return null;
}
const dwellSecondsOf = (document2) => {
var _a2;
const frame = document2.querySelector(courseConfig().timedReadFrame);
const src = (frame == null ? void 0 : frame.getAttribute("src")) ?? "";
const raw = (_a2 = /[?&]timing=(\d+)/u.exec(src)) == null ? void 0 : _a2[1];
const timing = raw ? Number.parseInt(raw, 10) : TIMED_READ_FALLBACK_SECONDS;
const seconds = Number.isFinite(timing) ? timing : TIMED_READ_FALLBACK_SECONDS;
return (seconds + TIMED_READ_SLACK_SECONDS) * TIMED_READ_ROUNDS;
};
const moduleOf = (attachment) => {
var _a2;
return typeof ((_a2 = attachment.property) == null ? void 0 : _a2.module) === "string" ? attachment.property.module : "";
};
const chapterTestDone = (root) => {
const status = root.querySelector(courseConfig().chapterTestStatus);
if (!status) return null;
const text = flattened$1(status.textContent);
return status.classList.contains(courseConfig().chapterTestDoneClass) || text.includes(courseConfig().chapterTestDoneText) || // 「待批阅」= 卷子已经交出去、只是还没批。对刷课与自动提交这两条链来说
// 它和「已完成」等价:没有可答的题了,也不该再交一次。
courseConfig().chapterTestSubmittedTexts.some(
(sample) => text.includes(sample)
);
};
const skipForSiteState = (state, kind, frame) => {
if (kind === "chapter-test" && frame && chapterTestDone(frame) === true)
return "test-done";
if (state === "job") return null;
if (state === "passed") return "passed";
if (kind !== "chapter-test") return "not-a-job";
const done = frame ? chapterTestDone(frame) : null;
if (done === null) return "not-a-job";
return done ? "test-done" : null;
};
function surveyTasks(documents) {
const attachments = courseAttachments(documents);
const framesByJobId = /* @__PURE__ */ new Map();
for (const document2 of documents) {
const jobId = frameJobId(document2);
if (!jobId) continue;
const bucket = framesByJobId.get(jobId);
if (bucket) bucket.push(document2);
else framesByJobId.set(jobId, [document2]);
}
const frameFor = (jobId) => {
const bucket = framesByJobId.get(jobId);
if (!bucket) return void 0;
return bucket.find((document2) => taskKindOf(document2)) ?? bucket[0];
};
const tasks = [];
const claimed = /* @__PURE__ */ new Set();
const root = documents[0];
for (const attachment of attachments ?? []) {
const jobId = attachmentJobId(attachment);
if (!jobId) continue;
const frame = frameFor(jobId);
for (const document22 of framesByJobId.get(jobId) ?? []) claimed.add(document22);
const kind = (frame ? taskKindOf(frame) : null) ?? courseConfig().moduleKind[moduleOf(attachment)] ?? "unknown";
const document2 = frame ?? root;
tasks.push({
document: document2,
kind,
jobId,
name: attachmentName(attachment) || KIND_LABEL[kind],
skip: skipForSiteState(siteStateOf(attachment), kind, frame),
dwellSeconds: kind === "timed-read" ? dwellSecondsOf(document2) : 0,
key: jobId
});
}
const declaredPending = tasks.some(
(task) => task.jobId !== null && task.skip === null
);
for (const document2 of documents) {
if (claimed.has(document2)) continue;
const kind = taskKindOf(document2);
if (!kind) continue;
const jobId = frameJobId(document2);
if (jobId && tasks.some((task) => task.jobId === jobId)) continue;
const key = jobId ?? `${kind}#${tasks.length}`;
tasks.push({
document: document2,
kind,
jobId,
name: KIND_LABEL[kind],
// 两道「别重做」的闸,任一成立就不做:
// ① 站点清单可读且已无未完成任务点 → 这一帧没有活可干;
// ② 这一帧所属的任务点块上写着「任务点已完成」。
// 都不成立才记 null(按「要做」处理)。只在这条兜底路径上用,
// 站点数据能对上时一律以站点为准。
skip: attachments !== null && !declaredPending ? "section-clear" : frameMarkedDone(document2) ? "marked-done" : null,
dwellSeconds: kind === "timed-read" ? dwellSecondsOf(document2) : 0,
key
});
}
return {
authoritative: attachments !== null,
declared: (attachments == null ? void 0 : attachments.length) ?? 0,
tasks
};
}
const inputValue = (document2, selector) => {
const element = document2.querySelector(selector);
if (!element) return "";
const view = element.ownerDocument.defaultView;
if (view && element instanceof view.HTMLInputElement) return element.value;
return element.getAttribute("value") ?? "";
};
function sectionCursor(documents) {
for (const document2 of documents) {
const courseId = inputValue(document2, courseConfig().cursorCourseId);
const chapterId = inputValue(document2, courseConfig().cursorChapterId);
const clazzId = inputValue(document2, courseConfig().cursorClazzId);
if (!courseId || !chapterId || !clazzId) continue;
return {
courseId,
chapterId,
clazzId,
tabCount: document2.querySelectorAll(courseConfig().sectionTabs).length,
document: document2
};
}
return null;
}
function advanceSectionViaSite(documents) {
const cursor = sectionCursor(documents);
if (!cursor) return false;
const pageWindow = courseWindow(cursor.document);
const counter = pageWindow == null ? void 0 : pageWindow.PCount;
if (typeof (counter == null ? void 0 : counter.next) !== "function") return false;
try {
counter.next(
String(cursor.tabCount),
cursor.chapterId,
cursor.courseId,
cursor.clazzId,
""
);
return true;
} catch {
return false;
}
}
const CHAPTER_ID_PATTERN = /\('(.*)','(.*)','(.*)'\)/u;
function chapterInfos(documents) {
for (const document2 of documents) {
const elements = [...document2.querySelectorAll(courseConfig().chapter)];
if (elements.length === 0) continue;
return elements.map((element) => {
var _a2;
const parent = element.parentElement;
const counter = parent == null ? void 0 : parent.querySelector(courseConfig().jobUnfinishCount);
const view = counter == null ? void 0 : counter.ownerDocument.defaultView;
const raw = view && counter instanceof view.HTMLInputElement ? counter.value : (counter == null ? void 0 : counter.getAttribute("value")) ?? "0";
return {
element,
chapterId: ((_a2 = CHAPTER_ID_PATTERN.exec(element.getAttribute("onclick") ?? "")) == null ? void 0 : _a2[3]) ?? null,
unfinishedCount: Number.parseInt(raw, 10) || 0,
active: (parent == null ? void 0 : parent.classList.contains("posCatalog_active")) ?? false
};
});
}
return [];
}
function nextUnfinishedChapter(chapters) {
const pending = chapters.filter(
(chapter) => chapter.unfinishedCount > 0 && !chapter.active
);
if (pending.length === 0) return null;
const activeIndex = chapters.findIndex((chapter) => chapter.active);
return pending.find((chapter) => chapters.indexOf(chapter) > activeIndex) ?? pending[0] ?? null;
}
function jumpToChapter(documents, chapter) {
var _a2;
const entry = (_a2 = chapter.element.parentElement) == null ? void 0 : _a2.querySelector(
courseConfig().chapterName
);
if (entry) {
try {
;
entry.click();
return true;
} catch {
}
}
const cursor = sectionCursor(documents);
if (!cursor || !chapter.chapterId) return false;
const pageWindow = courseWindow(cursor.document);
const jump = pageWindow == null ? void 0 : pageWindow.getTeacherAjax;
if (typeof jump !== "function") return false;
try {
;
jump(
cursor.courseId,
cursor.clazzId,
chapter.chapterId
);
return true;
} catch {
return false;
}
}
function isSpecialMode(documents) {
return documents.some(
(document2) => document2.querySelector(courseConfig().specialMode)
);
}
function advancePptSlide(document2) {
for (const audio of document2.querySelectorAll("audio"))
audio.muted = true;
const pageWindow = courseWindow(document2);
const next = pageWindow == null ? void 0 : pageWindow.swiperNext;
if (typeof next !== "function") return false;
try {
;
next();
return true;
} catch {
return false;
}
}
const pptSlideCount = (document2) => document2.querySelectorAll(courseConfig().pptSlide).length;
function startPlayer(document2) {
const direct = document2.querySelector(courseConfig().bigPlay);
const target = direct ?? [...document2.querySelectorAll("button, a, div, span")].find(
(element) => [
element.getAttribute("aria-label"),
element.getAttribute("title"),
element.textContent
].some((value) => (value ?? "").trim() === courseConfig().bigPlayLabel)
);
if (!target) return false;
try {
;
target.click();
return true;
} catch {
return false;
}
}
function openHyperlink(document2) {
const link = document2.querySelector("#hyperlink");
if (!link) return false;
const element = link;
const previous = element.onclick;
try {
element.onclick = () => false;
element.click();
return true;
} catch {
return false;
} finally {
element.onclick = previous;
}
}
const MAX_PLAYBACK_RATE = 2;
const hasFaceRecognition = (doc) => {
for (const img of doc.querySelectorAll(courseConfig().faceLegacy))
if (img.getAttribute("src")) return true;
for (const mask of doc.querySelectorAll(courseConfig().faceMask)) {
const view = mask.ownerDocument.defaultView;
const display = mask instanceof ((view == null ? void 0 : view.HTMLElement) ?? HTMLElement) ? mask.style.display : "";
if (display !== "none") return true;
}
return false;
};
const flattened = (value) => (value ?? "").replace(/\s+/gu, "");
const taskAlreadyDone = (doc) => {
var _a2;
if (flattened((_a2 = doc.body) == null ? void 0 : _a2.textContent).includes(courseConfig().taskDoneText))
return true;
for (const el of doc.querySelectorAll("[aria-label], [title], [alt]")) {
for (const attr of ["aria-label", "title", "alt"])
if (flattened(el.getAttribute(attr)).includes(courseConfig().taskDoneText))
return true;
}
return false;
};
const hasPlayerError = (doc) => {
for (const dialog of doc.querySelectorAll(courseConfig().playerError)) {
const text = dialog.textContent ?? "";
if (courseConfig().playerErrorTexts.some((sample) => text.includes(sample)))
return true;
}
return false;
};
const NETWORK_LOADING = 2;
const isLoadingMedia = (media) => media.networkState === NETWORK_LOADING;
const hasLoadingMedia = (documents) => documents.some(
(doc) => [...doc.querySelectorAll("video, audio")].some(
(el) => isLoadingMedia(el)
)
);
function playMedia(pending, options) {
var _a2;
const rate = Math.min(
Math.max(options.playbackRate ?? 1, 1),
MAX_PLAYBACK_RATE
);
pending.volume = options.volume ?? 0;
pending.playbackRate = rate;
void ((_a2 = pending.play()) == null ? void 0 : _a2.catch(() => {
}));
if (pending.paused) return { kind: "blocked", reason: "not-playing" };
return { kind: "playing", rate };
}
function stepSurveyedTask(survey, documents, options) {
const handled = options.isHandled ?? (() => false);
const kindEnabled = options.isKindEnabled ?? (() => true);
const unfinished = survey.tasks.filter(
(task2) => isPendingTask(task2) && kindEnabled(task2.kind)
);
const actionable = unfinished.filter(
(task2) => task2.kind === "media" ? !allMediaEnded(task2.document) : !handled(task2.key)
);
if (actionable.length === 0)
return unfinished.length === 0 ? { kind: "all-done", declared: survey.declared } : {
kind: "section-stalled",
unfinished: unfinished.length,
names: unfinished.map((task2) => task2.name)
};
const task = actionable[0];
switch (task.kind) {
case "media": {
const media = playableMediaList([task.document]);
const pending = media.find((item) => !item.ended) ?? playableMediaList(documents).find((item) => !item.ended);
if (!pending) {
const live = documents.some(
(doc) => [...doc.querySelectorAll("video, audio")].some(
(item) => !item.paused
)
);
if (!live && documents.some((doc) => startPlayer(doc)))
return { kind: "starting", name: task.name, taskKey: task.key };
return { kind: "loading", taskKey: task.key };
}
return playMedia(pending, options);
}
case "chapter-test":
return {
kind: "answering",
name: task.name,
taskKey: task.key,
/**
* **判据是「卷子进 DOM 了吗」,不是「帧的 jobid 对不对」。**
*
* 原先按 jobid 认:帧上有 `data="{jobid}"` 就算已加载。可任务点 tab 没激活时,
* 那个 iframe 是**空壳**——`data` 属性在、卷子不在。于是 `frameLoaded` 报 true、
* 引擎不去切 tab、把一张空页让给答题引擎,面板一边说「轮到章节测验」,答题页
* 一边说「还没识别到题目」,白等满 60 拍(2026-08-20 真机:本节任务点 0/2,
* 日志「抓到 0 题 × 90」)。这正是 08-18 那次修复只修了一半的地方。
*
* 判据必须与**答题引擎**一致:`.TiMu` 光在不算数,判分页、待批阅页同样有它;
* 云端规则的 match 认的是 `.TiMu input[name^="answertype"]`(有作答控件)。
* 用同一条判据,才不会把一张不可答的卷面让出去然后干等一窗。
*/
frameLoaded: !!task.document.querySelector(
courseConfig().chapterTestAnswerable
)
};
case "hyperlink":
return openHyperlink(task.document) ? { kind: "hyperlink", name: task.name, taskKey: task.key } : { kind: "idle", taskKey: task.key };
case "ppt-audio":
return advancePptSlide(task.document) ? {
kind: "ppt-slide",
name: task.name,
total: pptSlideCount(task.document),
taskKey: task.key
} : { kind: "idle", taskKey: task.key };
case "timed-read":
return {
kind: "dwelling",
name: task.name,
remainingMs: task.dwellSeconds * 1e3,
taskKey: task.key
};
default:
return { kind: "idle", taskKey: task.key };
}
}
function stepMediaTask(documents, options) {
for (const doc of documents) {
if (hasFaceRecognition(doc))
return { kind: "blocked", reason: "face-recognition" };
if (hasPlayerError(doc)) return { kind: "blocked", reason: "media-error" };
if (doc.querySelector(courseConfig().videoQuiz))
return { kind: "blocked", reason: "video-quiz" };
}
const survey = surveyTasks(documents);
if (survey.authoritative) return stepSurveyedTask(survey, documents, options);
const media = playableMediaList(documents);
const markerDone = documents.slice(1).some(taskAlreadyDone);
if (markerDone && media.length <= 1) return { kind: "finished" };
const pending = media.find((item) => !item.ended);
if (!pending) {
if (media.length > 0) return { kind: "finished" };
return { kind: hasLoadingMedia(documents) ? "loading" : "idle" };
}
return playMedia(pending, options);
}
function taskTabs(documents) {
for (const doc of documents) {
const tabs = [
...doc.querySelectorAll(courseConfig().taskTab)
];
if (tabs.length === 0) continue;
return {
count: tabs.length,
activeIndex: tabs.findIndex((tab) => tab.classList.contains("active")),
tabs
};
}
return null;
}
function advanceTaskTab(documents) {
var _a2;
const found = taskTabs(documents);
if (!found || found.activeIndex < 0) return false;
const next = found.activeIndex + 1;
if (next >= found.count) return false;
(_a2 = found.tabs[next]) == null ? void 0 : _a2.click();
return true;
}
const NEXT_SECTION_TEXT = "下一节";
function nextSectionTarget(documents) {
for (const doc of documents) {
for (const el of doc.querySelectorAll("a, button, div, span, i")) {
if ((el.textContent ?? "").trim() === NEXT_SECTION_TEXT)
return el;
}
const fallback = doc.querySelector(courseConfig().nextSectionFallback);
if (fallback) return fallback;
}
return null;
}
function advanceSection(documents) {
const target = nextSectionTarget(documents);
if (!target) return false;
target.click();
return true;
}
function sectionSignature(documents) {
var _a2, _b;
const href = ((_b = (_a2 = documents[0]) == null ? void 0 : _a2.location) == null ? void 0 : _b.href) ?? "";
const tabs = taskTabs(documents);
const labels = (tabs == null ? void 0 : tabs.tabs.map((tab) => tab.textContent ?? "").join(",")) ?? "";
return `${href}|${labels}`;
}
const SCROLLABLE_SLACK_PX = 8;
const MAX_SCROLL_TARGETS = 2e3;
function simulateReading(documents) {
var _a2, _b, _c;
const summary = {
frames: documents.length,
scrolled: 0,
pagers: 0
};
for (const doc of documents) {
const pager = [...doc.querySelectorAll(courseConfig().readerPager)].find(
(el) => {
var _a3;
return ((_a3 = el.style) == null ? void 0 : _a3.zIndex) === courseConfig().activePagerZIndex;
}
);
if (pager) {
try {
pager.click();
summary.pagers += 1;
} catch {
}
}
try {
(_c = (_a2 = doc.defaultView) == null ? void 0 : _a2.scrollTo) == null ? void 0 : _c.call(_a2, 0, ((_b = doc.documentElement) == null ? void 0 : _b.scrollHeight) ?? 0);
} catch {
}
let touched = 0;
for (const el of doc.querySelectorAll("div, section, main")) {
if (++touched > MAX_SCROLL_TARGETS) break;
if (el.scrollHeight <= el.clientHeight + SCROLLABLE_SLACK_PX) continue;
try {
el.scrollTop = el.scrollHeight;
summary.scrolled += 1;
} catch {
}
}
}
return summary;
}
const chapterLabel = (chapter) => {
var _a2, _b;
const name = ((_b = (_a2 = chapter.element.parentElement) == null ? void 0 : _a2.querySelector(courseConfig().chapterName)) == null ? void 0 : _b.textContent) ?? chapter.element.textContent;
return (name ?? "").trim() || "下一个未完成章节";
};
function counterElementCount(documents) {
for (const document2 of documents) {
if (document2.querySelectorAll(courseConfig().chapter).length === 0) continue;
return document2.querySelectorAll(courseConfig().jobUnfinishCount).length;
}
return 0;
}
function courseCounter(documents) {
const chapters = chapterInfos(documents);
if (chapters.length === 0) return null;
if (counterElementCount(documents) === 0) return null;
return {
unfinished: chapters.reduce(
(sum, chapter) => sum + chapter.unfinishedCount,
0
)
};
}
function createChaoxingCourseAdapter() {
return {
step: stepMediaTask,
survey: surveyTasks,
courseCounter,
simulateReading,
navigate: {
tabs: taskTabs,
advanceTab: advanceTaskTab,
sectionSignature,
sectionCursor,
chapters: chapterInfos,
nextUnfinishedChapter,
jumpToChapter,
isSpecialMode,
// 站点入口优先、文本兜底——与旧 runMediaTask 里的 `viaSite || advance` 逐字等价。
advanceSection: (documents) => advanceSectionViaSite(documents) || advanceSection(documents),
chapterLabel
}
};
}
function courseAdapterFor(platform) {
if (platform === "chaoxing") return createChaoxingCourseAdapter();
return null;
}
const SUBMIT_CLASSES = ["btnBlueSubmit"];
const SUBMIT_HANDLERS = ["btnBlueSubmit"];
const SUBMIT_TEXTS = ["提交", "交卷", "确定提交"];
const isExamPage = (document2) => {
var _a2;
return (((_a2 = document2.location) == null ? void 0 : _a2.pathname) ?? "").includes("/exam");
};
const normalizedText = (element) => (element.textContent ?? "").replace(/\s+/gu, "");
const DEFAULT_SUBMIT_THRESHOLD = 0.8;
function trustedRatio(items, answerableCount) {
if (items.length === 0) return 0;
const denominator = Math.max(items.length, answerableCount ?? 0);
const trusted = items.filter((item) => item.filled && !item.random).length;
return trusted / denominator;
}
function shouldAutoSubmit(state) {
if (!state.enabled) return false;
if (state.items.length === 0) return false;
const threshold = state.threshold ?? DEFAULT_SUBMIT_THRESHOLD;
return trustedRatio(state.items, state.answerableCount) >= threshold;
}
const hasUnrecognizedQuestions = (state) => state.answerableCount != null && state.answerableCount > state.items.length;
function safeSubmitTarget(documents) {
if (documents.some(isExamPage)) return null;
const hits = [];
for (const document2 of documents) {
let list = [];
try {
list = [...document2.querySelectorAll("a, button, input")];
} catch {
continue;
}
for (const element of list) {
const text = normalizedText(element);
const value = element.getAttribute("value") ?? "";
if (!SUBMIT_TEXTS.some((label) => text === label || value === label))
continue;
const handler = element.getAttribute("onclick") ?? "";
const classMatch = SUBMIT_CLASSES.some(
(name) => element.classList.contains(name)
);
const handlerMatch = SUBMIT_HANDLERS.some(
(name) => handler.startsWith(name)
);
if (classMatch || handlerMatch) hits.push(element);
if (hits.length > 1) return null;
}
}
return hits[0] ?? null;
}
const CANDIDATE_TEXT = /^(提交|交卷|确定提交|确定|取消|关闭|暂时保存|保存并提交)$/u;
function submitCandidates(documents) {
const out = [];
for (const document2 of documents) {
let list = [];
try {
list = [...document2.querySelectorAll("a, button, input, div, span")];
} catch {
continue;
}
for (const element of list) {
const text = normalizedText(element) || element.getAttribute("value") || "";
if (!CANDIDATE_TEXT.test(text) || text.length > 12) continue;
const handler = element.getAttribute("onclick") ?? "";
out.push({
text,
tag: element.tagName.toLowerCase(),
className: element.className || "",
handler: handler.slice(0, 40),
textLock: SUBMIT_TEXTS.some((label) => text === label),
entryLock: SUBMIT_CLASSES.some((name) => element.classList.contains(name)) || SUBMIT_HANDLERS.some((name) => handler.startsWith(name))
});
if (out.length >= 12) return out;
}
}
return out;
}
const DEFAULT_CONFIRM_TIMEOUT_MS = 4e3;
const DEFAULT_VERIFY_TIMEOUT_MS = 6e3;
const POLL_MS = 200;
const CONFIRM_SETTLE_MS = 600;
const CONFIRM_CLICK_ATTEMPTS = 2;
const sleep = (ms) => new Promise((resolve) => {
globalThis.setTimeout(resolve, ms);
});
const ENTRY_HANDLER_NAMES = ["btnBlueSubmit"];
function pageSubmitQuotaExhausted(view) {
try {
const quota = view == null ? void 0 : view.reqLimit;
return typeof quota === "number" && quota < 0;
} catch {
return false;
}
}
const ENTRY_CHAIN_NAMES = [
"btnBlueSubmit",
"validateTimeNew",
"toadd",
"confirmSubmitWork"
];
function readFunction(view, name) {
if (!view) return null;
try {
const value = view[name];
return typeof value === "function" ? value : null;
} catch {
return null;
}
}
function entryHandlerSource(view) {
if (!view) return "";
const parts = [];
for (const name of ENTRY_CHAIN_NAMES) {
const handler = readFunction(view, name);
if (handler)
parts.push(
`${name}=${String(handler).replace(/\s+/gu, " ").slice(0, 600)}`
);
}
return parts.join(" ⏎ ");
}
const CONFIRM_FUNCTION_NAMES = ["submitCheckTimes", "confirmSubmitWork"];
function readScalar(view, key) {
try {
return String(view[key]);
} catch {
return "读不到";
}
}
function pageSubmitLocked(view) {
if (!view) return false;
try {
const lock = view.submitLock;
return typeof lock === "number" && lock !== 0;
} catch {
return false;
}
}
function captureSiteMessage(view) {
let message = "";
const restores = [];
const patch = (target, key, read) => {
let original;
try {
original = target[key];
} catch {
return;
}
if (typeof original !== "function") return;
try {
target[key] = (...args) => {
if (!message) message = read(args).slice(0, 160);
return original.apply(target, args);
};
restores.push(() => {
try {
target[key] = original;
} catch {
}
});
} catch {
}
};
patch(view, "alert", (args) => String(args[0] ?? ""));
try {
const jquery = view.$;
if (jquery)
patch(
jquery,
"toast",
(args) => {
var _a2;
return String(((_a2 = args[0]) == null ? void 0 : _a2.content) ?? "");
}
);
} catch {
}
return {
message: () => message,
restore: () => {
for (const undo of restores) undo();
}
};
}
function answeredFieldSummary(view) {
var _a2;
try {
const doc = view.document;
const form = (_a2 = doc == null ? void 0 : doc.forms) == null ? void 0 : _a2.namedItem("form1");
if (!form) return "form=读不到";
const answers = [...form.querySelectorAll('input[name^="answer"]')];
const filled = answers.filter(
(el) => (el.value ?? "").trim() !== ""
).length;
const empty = [...form.elements].map((el) => el).filter((el) => el.name && !String(el.value ?? "").trim()).map((el) => el.name).slice(0, 8);
return `ans=${filled}/${answers.length} · 空[${empty.join(",") || "无"}]`;
} catch {
return "form=读不到";
}
}
function describeViews(views) {
return views.map((view, index) => {
if (!view) return `帧${index}:读不到`;
const found = [...CONFIRM_FUNCTION_NAMES, ...ENTRY_HANDLER_NAMES].filter(
(name) => readFunction(view, name)
);
return `帧${index}:${found.length ? found.join("+") : "无"}`;
}).join(" · ");
}
function confirmCandidateViews(documents, ...elements) {
const views = pageWindowsInFrameTree().map(
(window2) => window2
);
for (const document2 of [
...documents,
...elements.map((el) => el.ownerDocument)
]) {
try {
views.push(
pageWindowForDocument(document2)
);
} catch {
}
}
return views;
}
async function pollFor(probe, timeoutMs, stepMs) {
for (let waited = 0; waited <= timeoutMs; waited += stepMs) {
const hit = probe();
if (hit) return hit;
if (waited + stepMs > timeoutMs) break;
await sleep(stepMs);
}
return null;
}
function findWorkFrame(documents, ...elements) {
for (const win of confirmCandidateViews(documents, ...elements)) {
if (!win) continue;
if (readFunction(win, "btnBlueSubmit")) return { win };
}
return null;
}
const CONFIRM_OK_ID = "popok";
function visible(element) {
var _a2;
const view = (_a2 = element.ownerDocument) == null ? void 0 : _a2.defaultView;
if (!view) return false;
try {
let node = element;
while (node) {
const style = view.getComputedStyle(node);
if (style.display === "none" || style.visibility === "hidden")
return false;
node = node.parentElement;
}
return true;
} catch {
return false;
}
}
function findConfirmButton(documents) {
for (const document2 of documents) {
let element = null;
try {
element = document2.getElementById(CONFIRM_OK_ID);
} catch {
continue;
}
if (element && visible(element)) return element;
}
return null;
}
async function autoSubmitRound(getDocuments, state) {
var _a2, _b, _c, _d, _e, _f, _g, _h, _i;
if (!state.enabled) return "off";
if (state.items.length === 0) return "no-items";
if (!shouldAutoSubmit(state))
return hasUnrecognizedQuestions(state) ? "unrecognized-questions" : "below-threshold";
if (getDocuments().some(isExamPage)) return "exam-page";
const target = safeSubmitTarget(getDocuments());
if (!target) return "no-entry";
const frame = findWorkFrame(getDocuments(), target);
if (!frame) return "no-page-window";
if (pageSubmitQuotaExhausted(frame.win)) return "site-quota";
if (pageSubmitLocked(frame.win)) return "site-locked";
(_a2 = state.onEntry) == null ? void 0 : _a2.call(state, "click", entryHandlerSource(frame.win));
const siteMessage = captureSiteMessage(frame.win);
try {
try {
target.click();
} catch (error) {
(_b = state.onConfirmProbe) == null ? void 0 : _b.call(
state,
`点击提交入口抛 ${String((error == null ? void 0 : error.message) ?? error).slice(0, 160)}`
);
return "click-failed";
}
const confirmButton = await pollFor(
() => findConfirmButton(getDocuments()),
state.confirmTimeoutMs ?? DEFAULT_CONFIRM_TIMEOUT_MS,
state.pollMs ?? POLL_MS
);
if (!confirmButton) {
(_c = state.onConfirmProbe) == null ? void 0 : _c.call(
state,
`点了入口但没等到确认框 #${CONFIRM_OK_ID} · ${describeViews([frame.win])}`
);
return "clicked-entry";
}
(_d = state.onConfirmProbe) == null ? void 0 : _d.call(
state,
`确认框已出现 · ${answeredFieldSummary(frame.win)} · lock=${readScalar(frame.win, "submitLock")}`
);
await sleep(CONFIRM_SETTLE_MS);
let clicked = false;
for (let attempt = 0; attempt < CONFIRM_CLICK_ATTEMPTS; attempt += 1) {
const button = findConfirmButton(getDocuments());
if (!button) break;
try {
button.click();
} catch (error) {
(_e = state.onConfirmProbe) == null ? void 0 : _e.call(
state,
`点确认框抛 ${String((error == null ? void 0 : error.message) ?? error).slice(0, 160)}`
);
return "confirm-unverified";
}
if (!clicked) (_f = state.onConfirmCall) == null ? void 0 : _f.call(state, `#${CONFIRM_OK_ID}`);
clicked = true;
await sleep(CONFIRM_SETTLE_MS);
}
if (!clicked)
(_g = state.onConfirmProbe) == null ? void 0 : _g.call(state, "确认框在点到之前就消失了 · 没点成,不当作已确认");
const confirmStuck = findConfirmButton(getDocuments()) !== null;
if (confirmStuck)
(_h = state.onConfirmProbe) == null ? void 0 : _h.call(
state,
`点完 #${CONFIRM_OK_ID} 后框仍在 · 处理器没接住这一下`
);
const settled = () => clicked && !confirmStuck && !siteMessage.message() ? "confirm-accepted" : "confirm-unverified";
if (!state.isSubmitted) return settled();
const done = await pollFor(
() => {
var _a3;
return ((_a3 = state.isSubmitted) == null ? void 0 : _a3.call(state)) ? "submitted" : siteMessage.message() ? "refused" : null;
},
state.verifyTimeoutMs ?? DEFAULT_VERIFY_TIMEOUT_MS,
state.pollMs ?? POLL_MS
);
const refusal = siteMessage.message();
if (refusal) (_i = state.onConfirmProbe) == null ? void 0 : _i.call(state, `站点拒绝提交 · ${refusal}`);
return done === "submitted" ? "submitted" : settled();
} finally {
siteMessage.restore();
}
}
const MEMORY_GUARD_THRESHOLD_BYTES = 600 * 1024 * 1024;
const MEMORY_GUARD_CONSECUTIVE = 2;
function createMemoryGuard(options) {
const threshold = options.thresholdBytes ?? MEMORY_GUARD_THRESHOLD_BYTES;
const needed = options.consecutive ?? MEMORY_GUARD_CONSECUTIVE;
let streak = 0;
let fired = false;
return {
check() {
if (fired) return null;
const used = options.sample();
if (used === null || used < threshold) {
streak = 0;
return null;
}
streak += 1;
if (streak < needed) return null;
fired = true;
return used;
}
};
}
const readUsedJsHeap = (view) => {
return () => {
var _a2;
const used = (_a2 = view.performance.memory) == null ? void 0 : _a2.usedJSHeapSize;
return typeof used === "number" ? used : null;
};
};
const HTML_TAG_NAMES = new Set(
"a abbr address article aside audio b blockquote body br button canvas caption cite code col colgroup data datalist dd del details dialog div dl dt em fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup hr html i iframe img input ins kbd label legend li link main map mark menu meta meter nav noscript object ol optgroup option output p picture pre progress q rp rt ruby s samp script section select slot small source span strong style sub summary sup table tbody td template textarea tfoot th thead time title tr track u ul var video wbr".split(
" "
)
);
const redactOrdinaryTags = (value) => value.replace(
/<(\/?)([A-Za-z][A-Za-z0-9:-]*)([^<>]*)>/g,
(match, closing, tagName, rawAttributeText) => {
if (!HTML_TAG_NAMES.has(tagName.toLowerCase())) return match;
const attributeText = rawAttributeText.trim().replace(/\/$/, "").trim();
if (!attributeText) return "[标签]";
if (!closing && attributeText.includes("=")) return "[标签]";
return match;
}
);
const redactPreviewText = (value) => redactOrdinaryTags(
value.replace(//g, "[标签]").replace(/]*)?\s*>/gi, "[标签]")
).replace(/https?:\/\/[^\s<>"']+/gi, "[链接]");
const previewOf = (value) => parseQuestionContent(value, { stripUntrustedTags: false }).map(
(part) => part.type === "image" ? "[图片]" : redactPreviewText(part.value)
).join("").slice(0, 30);
const imageCountOf = (value) => parseQuestionContent(value).filter((part) => part.type === "image").length;
function itemOf(input) {
return {
type: input.type,
decodeFailed: input.decodeFailed ?? false,
stemPreview: previewOf(input.stem),
optionCount: input.options.length,
imageCount: imageCountOf(input.stem) + input.options.reduce((count2, option) => count2 + imageCountOf(option), 0),
unsupportedReason: input.unsupportedReason
};
}
async function runDiagnostic(adapter, ctx) {
var _a2;
if (!adapter.match(ctx))
return {
matched: false,
count: 0,
imageCount: 0,
harvestedCount: 0,
items: []
};
const items = (await adapter.captureTrees(ctx)).flatMap(
(tree) => flattenQuestionTree(tree.root).map(
(unit) => itemOf({
type: unit.queryType === "short_answer" ? QuestionType.Fill : unit.queryType,
stem: unit.effectiveStem,
options: unit.options.map((option) => option.content)
})
)
);
return {
matched: true,
count: items.length,
imageCount: items.reduce((count2, item) => count2 + item.imageCount, 0),
harvestedCount: ((_a2 = adapter.takeHarvested) == null ? void 0 : _a2.call(adapter).length) ?? 0,
items
};
}
function formatTime(d = /* @__PURE__ */ new Date()) {
const p = (n) => String(n).padStart(2, "0");
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
}
function createLogBuffer(max = 200) {
const entries = [];
return {
add(content, type = "info") {
const last = entries.at(-1);
if (last && last.content === content && last.type === type) {
last.repeat += 1;
last.time = formatTime();
return last;
}
const entry = {
time: formatTime(),
type,
content,
repeat: 1
};
entries.push(entry);
if (entries.length > max) entries.splice(0, entries.length - max);
return entry;
},
clear() {
entries.length = 0;
},
list() {
return entries;
}
};
}
function filterLogs(entries, level) {
if (level === "all") return [...entries];
return entries.filter((e) => e.type === level);
}
async function fetchMe(transport, baseUrl) {
try {
const response = await transport.send({
url: baseUrl + ME_PATH,
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
v: SCRIPT_VERSION
},
body: JSON.stringify({}),
timeoutMs: 8e3
});
const parsed = MeResponseSchema.safeParse(JSON.parse(response.body));
if (!parsed.success || parsed.data.code !== AiAskCode.Ok) return null;
const { username, balance, emailBound } = parsed.data;
if (typeof username !== "string" || typeof balance !== "number" || typeof emailBound !== "boolean")
return null;
return { username, balance, emailBound };
} catch {
return null;
}
}
const MESSAGE = {
[AiAskCode.Invalid]: "卡密无效、已用或已过期",
[AiAskCode.Unauthorized]: "登录已失效,请重新登录",
[AiAskCode.RateLimited]: "操作太频繁,请稍后再试",
[AiAskCode.Busy]: "服务繁忙,请稍后重试"
};
async function redeemCard(transport, code, baseUrl) {
try {
const res = await transport.send({
url: baseUrl + REDEEM_PATH,
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
v: SCRIPT_VERSION
},
body: JSON.stringify({ code }),
timeoutMs: 8e3
});
const parsed = RedeemResponseSchema.safeParse(JSON.parse(res.body));
if (!parsed.success) return { message: MESSAGE[AiAskCode.Busy] };
const { code: rc, balance } = parsed.data;
if (rc === AiAskCode.Ok && typeof balance === "number")
return { balance, message: "ok" };
if (rc === AiAskCode.Unauthorized)
return {
message: MESSAGE[AiAskCode.Unauthorized],
unauthorized: true
};
return { message: MESSAGE[rc] ?? MESSAGE[AiAskCode.Busy] };
} catch {
return { message: MESSAGE[AiAskCode.Busy] };
}
}
function buildReportIdentity(platform, clientId, scriptVersion, engineVersion, diagnostic) {
return {
platform,
clientId,
scriptVersion,
engineVersion,
rule: {
packageId: diagnostic.packageId,
variantId: diagnostic.variantId ?? "unresolved",
source: diagnostic.source,
version: diagnostic.version,
releaseSequence: diagnostic.releaseSequence,
contentHash: diagnostic.contentHash,
...diagnostic.release ? {
release: {
releaseId: diagnostic.release.releaseId,
channel: diagnostic.release.channel,
rolloutPercent: diagnostic.release.rolloutPercent,
cohortBucket: diagnostic.release.cohortBucket
}
} : {}
}
};
}
function buildMissingRuleReportIdentity(platform, clientId, scriptVersion, engineVersion, packageId) {
return {
platform,
clientId,
scriptVersion,
engineVersion,
rule: {
packageId,
variantId: "missing",
source: "missing",
version: "missing",
releaseSequence: 0,
contentHash: "missing"
}
};
}
const failedStage = (stage, reason) => ({ stage, ok: false, reason });
const unsafeReason = (value) => {
switch (value) {
case "missing-binding":
return "missing_binding";
case "disconnected":
return "disconnected";
case "stale":
return "stale_dom";
case "ambiguous-binding":
return "ambiguous_binding";
case "shape-mismatch":
return "shape_mismatch";
case "atomic-tree-blocked":
return "partial_not_allowed";
case "adapter-rejected":
return "adapter_rejected";
default:
return "unsafe_answer";
}
};
const CAPTURE_FAILURE_REASONS = {
timeout: "timeout",
budget_exceeded: "budget_exceeded",
call_depth_exceeded: "budget_exceeded",
unknown_primitive: "unknown_primitive"
};
function captureFailureReason(code) {
if (!code) return void 0;
return CAPTURE_FAILURE_REASONS[code] ?? "rule_failed";
}
function deriveStages(matched, list, autoFill, captureFailure) {
const stages = [
matched ? { stage: "match", ok: true } : failedStage("match", "no_match")
];
if (!matched) return stages;
stages.push(
list.length > 0 ? { stage: "capture", ok: true } : failedStage("capture", captureFailure ?? "zero_question")
);
if (list.length === 0) return stages;
const decodeFailed = list.some((it) => it.status === "decodeFail");
stages.push(
decodeFailed ? failedStage("decode", "decode_failed") : { stage: "decode", ok: true }
);
const queryable = list.filter(
(it) => it.status !== "decodeFail" && it.status !== "unsupported"
);
if (queryable.length > 0)
stages.push(
queryable.some((it) => it.status === "pending") ? failedStage("query", "query_failed") : { stage: "query", ok: true }
);
const unsafe = list.find(
(it) => it.status === "unsafe" || it.unsafeReason !== void 0
);
const safetyRelevant = list.some(
(it) => it.status === "hit" || it.status === "unsafe"
);
if (unsafe)
stages.push(failedStage("safety", unsafeReason(unsafe.unsafeReason)));
else if (list.some((it) => it.status === "unsupported"))
stages.push(failedStage("safety", "unsupported_question"));
else if (safetyRelevant) stages.push({ stage: "safety", ok: true });
const hits = list.filter((it) => it.status === "hit");
if (autoFill && hits.length > 0)
stages.push(
hits.every((it) => it.filled) ? { stage: "fill", ok: true } : failedStage(
"fill",
hits.some((it) => it.unsafeReason === "adapter-rejected") ? "adapter_rejected" : "fill_failed"
)
);
return stages;
}
function buildHealthReport(identity, matched, list, autoFill, captureFailure) {
return {
schemaVersion: 2,
...identity,
mode: "health",
stages: deriveStages(matched, list, autoFill, captureFailure)
};
}
function buildDiagnosticReport(identity, result) {
const stages = [
result.matched ? { stage: "match", ok: true } : failedStage("match", "no_match")
];
if (result.matched) {
stages.push(
result.count > 0 ? { stage: "capture", ok: true } : failedStage("capture", "zero_question")
);
if (result.count > 0)
stages.push(
result.items.some((i) => i.decodeFailed) ? failedStage("decode", "decode_failed") : { stage: "decode", ok: true }
);
}
return {
schemaVersion: 2,
...identity,
mode: "diagnostic",
stages,
diagnostic: {
matched: result.matched,
count: result.count,
imageCount: result.imageCount,
items: result.items.map((item) => ({
type: item.type,
decodeFailed: item.decodeFailed,
optionCount: item.optionCount,
imageCount: item.imageCount,
unsupportedReason: item.unsupportedReason
}))
}
};
}
async function sendReport(transport, baseUrl, req) {
try {
await transport.send({
method: "POST",
url: baseUrl + REPORT_PATH,
headers: {
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID()
},
body: JSON.stringify(req),
timeoutMs: 5e3
});
} catch {
}
}
const PLATFORM_LABEL = Object.freeze({
chaoxing: "超星",
wangxiao: "168 网校",
aopeng: "奥鹏教育"
});
const platformLabelFor = (platform) => PLATFORM_LABEL[platform] ?? platform;
const PLATFORM_CEILING = Object.freeze({
chaoxing: Object.freeze([
"answer",
"harvest",
"course-automation"
]),
wangxiao: Object.freeze(["answer", "harvest"]),
aopeng: Object.freeze(["harvest"])
});
const FALLBACK_FEATURES = Object.freeze([
"answer",
"harvest"
]);
function platformFeatures(platform, declared) {
const ceiling = PLATFORM_CEILING[platform] ?? FALLBACK_FEATURES;
return ceiling;
}
const MAX_RELEASE_CONTEXTS = MAX_RULE_PACKAGES * 3;
function parseSnapshot(input) {
if (!input || typeof input !== "object")
throw new Error("rule release context snapshot must be an object");
const snapshot2 = input;
if (snapshot2.schemaVersion !== 1 || !Array.isArray(snapshot2.summaries) || snapshot2.summaries.length > MAX_RELEASE_CONTEXTS || Object.keys(snapshot2).some(
(key) => key !== "schemaVersion" && key !== "summaries"
))
throw new Error("invalid rule release context snapshot");
return snapshot2.summaries.map(
(summary) => RulePackageSummarySchema.parse(summary)
);
}
const rulePackageIdentity = (value) => `${value.packageId}\0${value.releaseSequence}\0${value.contentHash}`;
function deduplicate(summaries) {
const byIdentity = /* @__PURE__ */ new Map();
for (const summary of summaries)
byIdentity.set(rulePackageIdentity(summary), summary);
return [...byIdentity.values()].sort(
(left, right) => left.packageId.localeCompare(right.packageId) || left.releaseSequence - right.releaseSequence
);
}
class GmRuleReleaseContextPersistence {
constructor(storage) {
__publicField(this, "key", "aiask_rule_release_context_v1");
this.storage = storage;
}
save(summaries) {
this.storage.set(this.key, {
schemaVersion: 1,
summaries: deduplicate(summaries)
});
}
load() {
const input = this.storage.get(this.key);
if (input == null) return [];
try {
return deduplicate(parseSnapshot(input));
} catch {
this.storage.delete(this.key);
return [];
}
}
clear() {
this.storage.delete(this.key);
}
}
const sourceLabels = {
"remote-active": "远程生效",
"remote-lkg": "远程回退"
};
const loadStatusLabels = {
loaded: "已验证本地规则快照",
"no-rules": "尚未同步云端规则",
"discarded-invalid-cache": "无效规则快照已清除",
"verification-deferred": "规则快照待验证,暂无可用规则"
};
function ruleCaptureFailure(adapter) {
return adapter instanceof JsonRulePlatformAdapter ? adapter.ruleDiagnostics().captureFailure : null;
}
function zeroQuestionReadout(platformLabel, captureFailure) {
return captureFailure ? {
log: `命中${platformLabel} · 抓到 0 题 · 规则捕获失败 ${captureFailure}`,
level: "warning"
} : { log: `命中${platformLabel} · 抓到 0 题`, level: "info" };
}
function resolvedRulePackage(adapter) {
var _a2;
if (!(adapter instanceof JsonRulePlatformAdapter)) return null;
return ((_a2 = adapter.ruleDiagnostics().resolved) == null ? void 0 : _a2.pkg) ?? null;
}
function buildRuleSessionDiagnostic(adapter, loadStatus, releaseSummaries = []) {
var _a2, _b;
if (!(adapter instanceof JsonRulePlatformAdapter)) return null;
const diagnostics = adapter.ruleDiagnostics();
const resolved = diagnostics.resolved;
if (!resolved) return null;
const releaseSummary = releaseSummaries.find(
(summary) => rulePackageIdentity(summary) === rulePackageIdentity(resolved.pkg)
);
return {
loadStatus,
loadStatusLabel: loadStatusLabels[loadStatus],
packageId: resolved.pkg.packageId,
variantId: diagnostics.variantId,
source: resolved.source,
sourceLabel: sourceLabels[resolved.source],
version: resolved.pkg.version,
releaseSequence: resolved.pkg.releaseSequence,
contentHash: resolved.pkg.contentHash,
...releaseSummary ? {
release: {
releaseId: releaseSummary.releaseId,
channel: releaseSummary.channel,
rolloutPercent: releaseSummary.rolloutPercent,
cohortBucket: releaseSummary.cohortBucket
}
} : {},
candidateVersion: (_a2 = diagnostics.store.candidate) == null ? void 0 : _a2.version,
lastKnownGoodVersion: (_b = diagnostics.store.lastKnownGood) == null ? void 0 : _b.version,
json: JSON.stringify(resolved.pkg, null, 2)
};
}
const isDefinitivelyInvalid = (error) => error instanceof RuleVerificationError || error instanceof RuleStoreError && error.code === "snapshot_invalid" || error instanceof Error && error.name === "ZodError";
class GmRuleStorePersistence {
constructor(storage) {
__publicField(this, "key", "aiask_rule_store_v1");
this.storage = storage;
}
save(store) {
this.storage.set(this.key, store.exportSnapshot());
}
async restore(store, verifier) {
const snapshot2 = this.storage.get(this.key);
if (snapshot2 == null) return "no-rules";
try {
await store.restoreSnapshot(snapshot2, verifier);
return "loaded";
} catch (error) {
if (isDefinitivelyInvalid(error)) {
this.storage.delete(this.key);
return "discarded-invalid-cache";
}
return "verification-deferred";
}
}
async load(options) {
const store = new RuleStore();
return { store, status: await this.restore(store, options.verifier) };
}
clear() {
this.storage.delete(this.key);
}
}
class GmRuleKeysetPersistence {
constructor(storage) {
__publicField(this, "key", "aiask_rule_keyset_v1");
this.storage = storage;
}
save(input) {
this.storage.set(this.key, ServerKeysetSchema.parse(input));
}
load() {
const input = this.storage.get(this.key);
return input == null ? null : ServerKeysetSchema.parse(input);
}
clear() {
this.storage.delete(this.key);
}
}
async function restoreCachedRuleStore(options) {
const persistence = new GmRuleKeysetPersistence(options.storage);
try {
const keyset = persistence.load();
if (!keyset) return options.runtime.initialize({ storage: options.storage });
if (!await verifyServerKeysetSignature(
await importEcdsaPublicJwk(options.rootPublicJwk),
keyset
))
throw new Error("cached keyset root signature rejected");
const highestAcceptedVersion = await readKeysetWatermark(
options.storage,
options.baseUrl,
options.inheritLegacyKeysetWatermark
);
validateServerKeyset(
keyset,
(options.now ?? Date.now)(),
highestAcceptedVersion
);
return options.runtime.initialize({
storage: options.storage,
verifier: options.createVerifier(keyset)
});
} catch {
persistence.clear();
return options.runtime.initialize({ storage: options.storage });
}
}
const RULE_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
const RULE_UPDATE_RECOVERY_RETRY_MS = 10 * 60 * 1e3;
const RULE_UPDATE_LAST_CHECK_KEY = "aiask_rule_update_last_check_v1";
function normalizedBaseUrl(value) {
return value.replace(/\/+$/u, "");
}
function knownPackages(runtime, store) {
return runtime.packageIds().flatMap((packageId) => {
const diagnostics = store.diagnostics(packageId);
const candidates = [
diagnostics.candidate,
diagnostics.active,
diagnostics.lastKnownGood
].filter((pkg) => pkg != null);
const current = candidates.sort(
(left, right) => right.releaseSequence - left.releaseSequence
)[0];
return current ? [
{
packageId: current.packageId,
releaseSequence: current.releaseSequence,
contentHash: current.contentHash
}
] : [];
});
}
function mergeKnown(known, rejected) {
const merged = new Map(known.map((item) => [item.packageId, item]));
for (const [packageId, item] of rejected) merged.set(packageId, item);
return [...merged.values()].slice(0, MAX_RULE_PACKAGES);
}
function errorReason(error) {
return error instanceof Error ? error.message : "rule update failed";
}
class RuleUpdater {
constructor(options) {
__publicField(this, "now");
__publicField(this, "baseUrl");
__publicField(this, "pending");
this.options = options;
this.now = options.now ?? (() => Date.now());
this.baseUrl = normalizedBaseUrl(options.baseUrl);
}
check(options = {}) {
if (this.pending) return this.pending;
this.pending = this.perform(options.force === true).finally(() => {
this.pending = void 0;
});
return this.pending;
}
async perform(force) {
const checkedAt = this.now();
const previous = this.options.storage.get(RULE_UPDATE_LAST_CHECK_KEY);
const interval = this.options.runtime.usablePackageIds().length > 0 ? RULE_UPDATE_INTERVAL_MS : RULE_UPDATE_RECOVERY_RETRY_MS;
if (!force && typeof previous === "number" && Number.isFinite(previous) && previous >= 0 && previous <= checkedAt && checkedAt - previous < interval)
return { status: "skipped", checkedAt, updatedPackageIds: [] };
try {
const keyset = await this.options.getKeyset();
let store = this.options.runtime.snapshot().store;
let verifier = this.options.createVerifier(keyset, store);
const initialized = await this.options.runtime.initialize({
storage: this.options.storage,
verifier
});
store = initialized.store;
verifier = this.options.createVerifier(
keyset,
store,
initialized.releaseSummaries
);
const persistence = new GmRuleStorePersistence(this.options.storage);
const updatedPackageIds = [];
const rejected = /* @__PURE__ */ new Map();
let failure;
for (let index = 0; index < MAX_RULE_PACKAGES; index += 1) {
const response = await this.options.transport.send({
url: `${this.baseUrl}${RULE_SYNC_PATH}`,
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
engineVersion: this.options.engineVersion,
known: mergeKnown(
knownPackages(this.options.runtime, store),
rejected
)
}),
timeoutMs: 8e3
});
if (response.status < 200 || response.status >= 300)
throw new Error("rule sync request failed");
const parsed = RuleSyncResponseSchema.parse(JSON.parse(response.body));
if (parsed.code !== AiAskCode.Ok)
throw new Error(`rule sync rejected: ${parsed.code}`);
if (parsed.update) {
const update = parsed.update;
try {
verifier = this.options.createVerifier(keyset, store, parsed.latest);
await store.stageRemote(update, verifier);
const candidate = store.diagnostics(update.packageId).candidate;
if ((candidate == null ? void 0 : candidate.releaseSequence) === update.releaseSequence && candidate.contentHash === update.contentHash) {
store.activateCandidate(update.packageId);
persistence.save(store);
updatedPackageIds.push(update.packageId);
}
} catch (error) {
failure ?? (failure = errorReason(error));
rejected.set(update.packageId, {
packageId: update.packageId,
releaseSequence: update.releaseSequence,
contentHash: update.contentHash
});
}
}
this.options.runtime.reconcileReleaseSummaries(
parsed.latest,
this.options.storage
);
if (!parsed.update) break;
if (!parsed.hasMore) break;
}
this.options.storage.set(RULE_UPDATE_LAST_CHECK_KEY, checkedAt);
return {
status: updatedPackageIds.length > 0 ? "updated" : failure ? "failed" : "up-to-date",
checkedAt,
updatedPackageIds,
...failure ? { reason: failure } : {}
};
} catch (error) {
this.options.storage.set(RULE_UPDATE_LAST_CHECK_KEY, checkedAt);
return {
status: "failed",
checkedAt,
updatedPackageIds: [],
reason: errorReason(error)
};
}
}
}
function ruleStorageKeys(storage) {
return [
new GmRuleStorePersistence(storage).key,
new GmRuleKeysetPersistence(storage).key,
new GmRuleReleaseContextPersistence(storage).key,
RULE_UPDATE_LAST_CHECK_KEY,
KEYSET_WATERMARKS_KEY,
HIGHEST_KEYSET_VERSION_KEY
];
}
function resetRuleStorage(storage) {
for (const key of ruleStorageKeys(storage)) storage.delete(key);
}
const deferredVerifier = {
verify: () => Promise.reject(new Error("rule verifier unavailable"))
};
function packageIdsFor(store) {
return [
...new Set(store.exportSnapshot().packages.map((entry) => entry.packageId))
].sort();
}
function retainedReleaseSummaries(store, packageIds, summaries) {
const allowed = /* @__PURE__ */ new Set();
for (const packageId of packageIds) {
const diagnostics = store.diagnostics(packageId);
for (const pkg of [
diagnostics.active,
diagnostics.lastKnownGood,
diagnostics.candidate
])
if (pkg) allowed.add(rulePackageIdentity(pkg));
}
return summaries.filter(
(summary) => allowed.has(rulePackageIdentity(summary))
);
}
class UserscriptRuleStoreRuntime {
constructor() {
__publicField(this, "state");
__publicField(this, "pending");
__publicField(this, "initialized", false);
this.state = {
store: new RuleStore(),
status: "no-rules",
releaseSummaries: []
};
}
snapshot() {
return this.state;
}
packageIds() {
return packageIdsFor(this.state.store);
}
/**
* 真正**能拿来跑**的包。`packageIds()` 数的是 `exportSnapshot()`,里面还含
* 只剩防回退水位的空条目与未激活的 candidate,`resolve()` 对它们一律返回 null。
*
* 2026-08-20 真机:面板页脚显示「规则 7 包」而答题引擎一条规则都取不到,
* 「有几个包」与「有没有规则可用」被当成同一个问题问了——它们不是。
* 节流判据与界面读数都只该看这一个。
*/
usablePackageIds() {
return this.packageIds().filter(
(packageId) => this.state.store.resolve(packageId) !== null
);
}
releaseSummaryFor(value) {
const identity = rulePackageIdentity(value);
return this.state.releaseSummaries.find(
(summary) => rulePackageIdentity(summary) === identity
) ?? null;
}
reconcileReleaseSummaries(latest, storage) {
const byIdentity = /* @__PURE__ */ new Map();
for (const summary of this.state.releaseSummaries)
byIdentity.set(rulePackageIdentity(summary), summary);
for (const summary of latest)
byIdentity.set(rulePackageIdentity(summary), summary);
const releaseSummaries = retainedReleaseSummaries(
this.state.store,
this.packageIds(),
[...byIdentity.values()]
);
this.state = { ...this.state, releaseSummaries };
new GmRuleReleaseContextPersistence(storage).save(releaseSummaries);
}
initialize(options) {
if (this.initialized) {
if (options.verifier && this.state.status === "verification-deferred")
return this.restore(options.storage, options.verifier);
return Promise.resolve(this.state);
}
if (this.pending) return this.pending;
this.pending = this.load(options).then((state) => {
this.state = state;
this.initialized = true;
this.pending = void 0;
return state;
});
return this.pending;
}
restore(storage, verifier) {
if (this.pending) return this.pending;
this.pending = new GmRuleStorePersistence(storage).restore(this.state.store, verifier).then((status) => {
const persistence = new GmRuleReleaseContextPersistence(storage);
const releaseSummaries = retainedReleaseSummaries(
this.state.store,
this.packageIds(),
persistence.load()
);
persistence.save(releaseSummaries);
this.state = { store: this.state.store, status, releaseSummaries };
this.pending = void 0;
return this.state;
});
return this.pending;
}
async load(options) {
const persistence = new GmRuleReleaseContextPersistence(options.storage);
const cachedReleaseSummaries = persistence.load();
try {
const loaded = await new GmRuleStorePersistence(options.storage).load({
verifier: options.verifier ?? deferredVerifier
});
const releaseSummaries = loaded.status === "verification-deferred" ? cachedReleaseSummaries : retainedReleaseSummaries(
loaded.store,
packageIdsFor(loaded.store),
cachedReleaseSummaries
);
if (loaded.status !== "verification-deferred")
persistence.save(releaseSummaries);
return { ...loaded, releaseSummaries };
} catch {
return {
store: new RuleStore(),
status: "verification-deferred",
releaseSummaries: cachedReleaseSummaries
};
}
}
}
const ruleStoreRuntime = new UserscriptRuleStoreRuntime();
const listeners = /* @__PURE__ */ new Set();
function subscribeRuleStoreUpdates(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
async function checkRulesAndNotify(check) {
const result = await check();
for (const listener of [...listeners]) {
try {
listener(result);
} catch {
}
}
return result;
}
function ruleUpdateReadout(result, packageCount) {
if (result.status === "skipped")
return packageCount === 0 ? {
note: "本地没有任何规则包",
log: "本地没有任何规则包 · 距上次检查不足 24 小时,点「检查更新」可立即重试",
level: "warning"
} : null;
if (result.status === "updated")
return {
note: `已更新 ${result.updatedPackageIds.length} 个规则包`,
log: `规则更新完成 · ${result.updatedPackageIds.join("、")}`,
level: "info"
};
if (result.status === "failed")
return {
note: "检查失败 · 当前规则继续可用",
log: `规则检查失败 · ${result.reason ?? "已保留当前规则"}`,
level: "warning"
};
if (packageCount === 0)
return {
note: "服务端没有可用规则包",
log: "规则同步成功 · 服务端没有下发任何规则包",
level: "warning"
};
return {
note: "当前规则已是最新",
log: "规则检查完成 · 已是最新",
level: "info"
};
}
const CHA0XING_PACKAGE_HOOKS = Object.freeze({
[CHA0XING_PACKAGE_IDS.examStudent]: Object.freeze([
"registerExamQuestion",
"prepareExamPlan",
"commitExamPlan"
]),
[CHA0XING_PACKAGE_IDS.dowork]: Object.freeze(["commitDoworkPlan"]),
[CHA0XING_PACKAGE_IDS.studentstudy]: Object.freeze([
"commitStudentstudyPlan"
]),
[CHA0XING_PACKAGE_IDS.oldHomework]: Object.freeze([
"commitOldHomeworkPlan"
]),
[CHA0XING_PACKAGE_IDS.oldChapter]: Object.freeze([
"commitOldChapterPlan"
]),
[CHA0XING_PACKAGE_IDS.newChapter]: Object.freeze([
"commitNewChapterPlan"
])
});
function authorizesRollback(summaries, authorization, pkg) {
return summaries.some(
(summary) => {
var _a2;
return summary.packageId === pkg.packageId && summary.version === pkg.version && summary.releaseSequence === pkg.releaseSequence && summary.contentHash === pkg.contentHash && ((_a2 = summary.rollbackAuthorization) == null ? void 0 : _a2.toVersion) === authorization.toVersion && summary.rollbackAuthorization.authorizationId === authorization.authorizationId;
}
);
}
function baseRegistry(options, policy) {
const refs = new RuntimeReferenceRegistry({
maxDomRefs: policy.limits.maxDomRefs
});
const capture2 = new RuleCaptureRegistry({
maxTrees: 256,
maxBindings: policy.limits.maxDomRefs
});
const registry = new PrimitiveRegistry();
registerCoreRulePrimitives(registry, {
document: options.document,
location: options.location,
refs,
capture: capture2,
writer: new BindingRegistryAnswerWriter(capture2.bindings),
resources: new RuleResourceScope()
});
return { registry, refs };
}
function platformContext(pkg, options) {
if (pkg.platform === "chaoxing") {
const { registry, refs } = baseRegistry(options, CHA0XING_RULE_POLICY);
const hooks = CHA0XING_PACKAGE_HOOKS[pkg.packageId] ?? [];
const on = (name) => hooks.includes(name);
registerChaoxingRuleHooks(registry, {
typr: options.typr,
table: options.fontTable ?? {},
refs,
resolveUeditorBodies: (targets) => targets,
...on("registerExamQuestion") ? { registerExamQuestion: () => void 0 } : {},
...on("prepareExamPlan") ? { prepareExamPlan: () => false } : {},
...on("commitExamPlan") ? { commitExamPlan: () => false } : {},
...on("commitDoworkPlan") ? { commitDoworkPlan: () => false } : {},
...on("commitStudentstudyPlan") ? { commitStudentstudyPlan: () => false } : {},
...on("commitOldHomeworkPlan") ? { commitOldHomeworkPlan: () => false } : {},
...on("commitOldChapterPlan") ? { commitOldChapterPlan: () => false } : {},
...on("commitNewChapterPlan") ? { commitNewChapterPlan: () => false } : {}
});
return { registry, policy: CHA0XING_RULE_POLICY };
}
const trustedRemote = trustedRemoteRulePlatformByPackageId(pkg.packageId);
if ((trustedRemote == null ? void 0 : trustedRemote.platform) === pkg.platform) {
const { registry } = baseRegistry(options, trustedRemote.policy);
if (pkg.platform === "aopeng")
registerAopengRuleHooks(registry, { readCapturedResponse: () => null });
return { registry, policy: trustedRemote.policy };
}
throw new RuleVerificationError(
"capability_denied",
`unsupported rule platform: ${pkg.platform}`
);
}
function createUserscriptRuleVerifier(options) {
return {
verify: async (input) => {
var _a2;
const pkg = RulePackageSchema.parse(input);
const { registry, policy } = platformContext(pkg, options);
const current = (_a2 = options.store.resolve(pkg.packageId)) == null ? void 0 : _a2.pkg;
return new RuleVerifier({
engineVersion: RULE_ENGINE_VERSION,
keyset: options.keyset,
registry,
policy,
services: options.services ?? RULE_EXPRESSION_SERVICES,
now: options.now,
authorizeRollback: (authorization, candidate) => authorizesRollback(
options.releaseSummaries ?? [],
authorization,
candidate
),
...current ? {
current: {
version: current.version,
releaseSequence: current.releaseSequence,
contentHash: current.contentHash
}
} : {}
}).verify(pkg);
}
};
}
const keysetPersistence = new GmRuleKeysetPersistence(gmRuleStorage);
const verifierFor = (keyset, store, releaseSummaries = new GmRuleReleaseContextPersistence(
gmRuleStorage
).load()) => createUserscriptRuleVerifier({
keyset,
store,
document,
location,
typr: Typr$1,
fontTable: getChaoxingFontTable(),
now: () => ruleSecurityClient.sessions.serverNow(),
releaseSummaries
});
const updater = new RuleUpdater({
transport: ruleTransport,
baseUrl: BACKEND_BASE_URL,
storage: gmRuleStorage,
runtime: ruleStoreRuntime,
engineVersion: RULE_ENGINE_VERSION,
getKeyset: async () => {
const keyset = (await ruleSecurityClient.sessions.getSession()).keyset;
keysetPersistence.save(keyset);
return keyset;
},
createVerifier: verifierFor
});
const initializeRuleStoreRuntime = () => restoreCachedRuleStore({
storage: gmRuleStorage,
runtime: ruleStoreRuntime,
baseUrl: BACKEND_BASE_URL,
inheritLegacyKeysetWatermark: IS_DEFAULT_BACKEND,
rootPublicJwk: SECURITY_ROOT_PUBLIC_JWK,
createVerifier: (keyset) => verifierFor(keyset, ruleStoreRuntime.snapshot().store)
});
const checkRuleUpdates = (force = false) => checkRulesAndNotify(() => updater.check({ force }));
const FREE_BANK_URL = "https://cx.icodef.com/wyn-nb?v=4";
const AD_KEYWORDS = ["叛逆", "公众号", "李恒雅", "一之"];
function isAdAnswer(text) {
return AD_KEYWORDS.some((k) => text.includes(k));
}
const NON_ANSWER_EXACT = /* @__PURE__ */ new Set([
"暂无KEY",
"无KEY",
"暂无APIKEY",
"无APIKEY",
"未配置KEY",
"未配置APIKEY",
"未填写KEY",
"未填写APIKEY",
"APIKEY缺失",
"请填写KEY",
"请填写APIKEY",
"请配置KEY",
"请配置APIKEY",
"暂无答案",
"暂无答案信息",
"无答案",
"没有答案",
"未找到答案",
"未查询到答案",
"未检索到答案",
"查询不到答案",
"请登录",
"请先登录",
"未登录",
"鉴权失败",
"未授权",
"无权限",
"请求失败",
"网络异常",
"网络请求失败",
"响应解析失败",
"接口异常",
"接口请求失败",
"接口请求超时",
"请求超时",
"次数不足",
"余额不足",
"额度不足"
]);
const NON_ANSWER_PATTERNS = [
/^暂未(收录|找到|查询到|检索到)(参考)?答案(信息)?$/,
/^未(找到|查询到|检索到)(参考)?答案(信息)?$/,
/^没有(找到|查询到|检索到)?(参考)?答案(信息)?$/,
/^请(先)?登录后(再)?(查看|使用|搜索|查询).*$/,
/^登录后才可以使用.*$/,
/^API\s*KEY\s*(缺失|未填写|未配置|无效).*$/i,
/^(请求|接口|网络).*(失败|异常|超时)$/,
/^(次数|余额|额度).*(不足|已用完)$/
];
function isNonAnswerText(value) {
const trimmed = value.trim();
if (!trimmed) return true;
if (NON_ANSWER_EXACT.has(trimmed.replace(/\s+/g, "").toUpperCase()))
return true;
return NON_ANSWER_PATTERNS.some((pattern) => pattern.test(trimmed));
}
function parseIcodefBody(raw) {
let res;
try {
res = JSON.parse(raw);
} catch {
return null;
}
if (res.code !== 1 || typeof res.data !== "string") return null;
const data = res.data.replace(/javascript:void\(0\);/g, "").trim().replace(/\n/g, "");
if (!data || isAdAnswer(data)) return null;
const values = data.split("#").map((s) => s.trim()).filter(Boolean);
return values.length ? values : null;
}
async function freeBankSearch(transport, unit, timeoutMs = 5e3) {
try {
const res = await transport.send({
url: FREE_BANK_URL,
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
question: questionTextForSearch(unit.effectiveStem)
}),
timeoutMs
});
if (res.status < 200 || res.status >= 300) return null;
const values = parseIcodefBody(res.body);
if (!values) return null;
if (unit.options.length === 0 && values.some(isNonAnswerText)) return null;
return { values };
} catch {
return null;
}
}
function createSession(o) {
var _a2;
const ctx = {
document: o.document,
location: o.location,
signal: new AbortController().signal,
deps: { typr: o.typr, table: o.fontTable }
};
const runtimeState = ruleStoreRuntime.snapshot();
const ruleStore = o.ruleStore ?? runtimeState.store;
const ruleStoreStatus = o.ruleStoreStatus ?? runtimeState.status;
const factories = o.adapterFactories ?? createDefaultAdapterFactories(
o.location,
o.typr,
ruleStore,
o.fontTable ?? {}
);
const candidates = factories.map((factory) => ({
factory,
adapter: factory()
}));
const adapter = new RuleRuntime(
candidates.map((candidate) => candidate.adapter)
).resolve(ctx);
const selected2 = candidates.find((candidate) => candidate.adapter === adapter);
if (!adapter || !selected2) return { session: null, reason: "unsupported" };
const client = new RelayClient(o.backendTransport, o.baseUrl);
const freeFirst = o.settings.freeFirst !== false;
const sessionDeps = {
...o.sessionDeps,
// 始终注入;开关靠 SessionOptions.freeFirst(可 setOptions 热切换)
freeSearch: (req) => freeBankSearch(o.transport, req),
canPaidSearch: () => !!o.getToken(),
localStore: ((_a2 = o.sessionDeps) == null ? void 0 : _a2.localStore) ?? o.localStore
};
const session = new AnswerSession(
adapter,
client,
{
autoFill: o.settings.autoFill,
delayMs: o.settings.delayMs,
freeFirst
},
sessionDeps,
o.emit
);
return {
session,
ctx,
platform: adapter.platform,
adapter,
createAdapter: selected2.factory,
rule: buildRuleSessionDiagnostic(
adapter,
ruleStoreStatus,
o.ruleReleaseSummaries ?? runtimeState.releaseSummaries
)
};
}
const _hoisted_1$1 = { class: "question-content" };
const _hoisted_2$1 = { key: 0 };
const _hoisted_3$1 = {
key: 1,
class: "image-failed"
};
const _hoisted_4$1 = ["src", "onError"];
const _sfc_main$1 = /* @__PURE__ */ vue.defineComponent({
__name: "QuestionContent",
props: {
content: {},
maxHeight: { default: "180px" }
},
setup(__props) {
const props = __props;
const generation = vue.ref(0);
const parts = vue.computed(() => {
const renderedGeneration = generation.value;
return parseQuestionContent(props.content).map((part) => ({
...part,
generation: renderedGeneration
}));
});
const failed2 = vue.ref(/* @__PURE__ */ new Set());
vue.watch(
() => props.content,
() => {
generation.value++;
failed2.value = /* @__PURE__ */ new Set();
}
);
const markFailed = (index, renderedGeneration) => {
if (renderedGeneration !== generation.value) return;
failed2.value = new Set(failed2.value).add(index);
};
return (_ctx, _cache) => {
return vue.openBlock(), vue.createElementBlock("span", _hoisted_1$1, [
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(parts.value, (part, index) => {
return vue.openBlock(), vue.createElementBlock(vue.Fragment, {
key: `${index}:${part.value}`
}, [
part.type === "text" ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_2$1, vue.toDisplayString(part.value), 1)) : failed2.value.has(index) ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_3$1, "图片加载失败")) : (vue.openBlock(), vue.createElementBlock("img", {
key: 2,
src: part.value,
alt: "题目图片",
loading: "lazy",
referrerpolicy: "no-referrer",
style: vue.normalizeStyle({ maxHeight: __props.maxHeight }),
onError: ($event) => markFailed(index, part.generation)
}, null, 44, _hoisted_4$1))
], 64);
}), 128))
]);
};
}
});
function formatClock(seconds) {
const total = Math.max(0, Math.floor(seconds));
const secs = String(total % 60).padStart(2, "0");
const mins = Math.floor(total / 60) % 60;
const hours = Math.floor(total / 3600);
if (hours > 0) return `${hours}:${String(mins).padStart(2, "0")}:${secs}`;
return `${mins}:${secs}`;
}
function coursePositionLine(progress) {
var _a2;
const position2 = (_a2 = progress == null ? void 0 : progress.task) == null ? void 0 : _a2.position;
if (!position2) return null;
return position2.totalSeconds === null ? formatClock(position2.currentSeconds) : `${formatClock(position2.currentSeconds)} / ${formatClock(position2.totalSeconds)}`;
}
function courseCountLine(progress) {
const parts = [];
const section = progress == null ? void 0 : progress.section;
if (section && section.total > 0)
parts.push(`本节任务点 ${section.done}/${section.total}`);
if (progress == null ? void 0 : progress.course) parts.push(`全课还剩 ${progress.course.unfinished} 个`);
return parts.length > 0 ? parts.join(" · ") : null;
}
const NOTHING_TO_DO_KINDS = /* @__PURE__ */ new Set([
"idle",
"all-done",
"advancing",
"advancing-section"
]);
function courseStatusLine(state, progress) {
if (!state) return "未开启";
if (state.kind === "playing" && (progress == null ? void 0 : progress.task))
return `正在播放「${progress.task.name}」`;
const section = progress == null ? void 0 : progress.section;
if (section && NOTHING_TO_DO_KINDS.has(state.kind)) {
const off = section.skipped.filter(
(item) => item.reason === "kind-off"
).length;
if (section.total === 0 && off > 0)
return `本节 ${off} 项都被你关掉的类型跳过了`;
}
if (state.kind !== "idle") return "";
if (!section) return "本页没有可播放的任务点";
if (section.done === section.total) return "本节任务点已全部完成";
return `本节还剩 ${section.total - section.done} 项 · 本页没找到可做的内容`;
}
function createPanelLauncherGestureState() {
return { suppressPointerClick: false };
}
function beginPanelLauncherGesture(state) {
state.suppressPointerClick = false;
}
function endPanelLauncherGesture(state, result, eventType) {
state.suppressPointerClick = result.moved;
return eventType === "pointerup" && !result.moved;
}
function consumePanelLauncherActivation(state, clickDetail) {
const activate = !state.suppressPointerClick || clickDetail === 0;
state.suppressPointerClick = false;
return activate;
}
function createPanelDragState() {
return {
pointerId: null,
offsetX: 0,
offsetY: 0,
panel: { width: 0, height: 0 },
origin: { x: 0, y: 0 },
moved: false
};
}
function isPanelDragInteractiveTarget(target) {
return target instanceof Element && target.closest('button,a,input,textarea,select,[role="button"]') !== null;
}
function beginPanelDrag(state, input) {
if (state.pointerId !== null) return false;
if (input.interactive) return false;
if (!input.isPrimary) return false;
if (input.pointerType === "mouse" && input.button !== 0) return false;
state.pointerId = input.pointerId;
state.offsetX = input.clientX - input.rect.left;
state.offsetY = input.clientY - input.rect.top;
state.panel = { width: input.rect.width, height: input.rect.height };
state.origin = {
x: Math.round(input.rect.left),
y: Math.round(input.rect.top)
};
state.moved = false;
return true;
}
function movePanelDrag(state, input, viewport) {
if (state.pointerId === null || input.pointerId !== state.pointerId) {
return null;
}
const position2 = clampPanelPosition(
{
x: input.clientX - state.offsetX,
y: input.clientY - state.offsetY
},
state.panel,
viewport
);
if (position2.x !== state.origin.x || position2.y !== state.origin.y) {
state.moved = true;
}
return position2;
}
function endPanelDrag(state, pointerId) {
if (state.pointerId === null || state.pointerId !== pointerId) {
return null;
}
const result = { moved: state.moved };
const reset = createPanelDragState();
state.pointerId = reset.pointerId;
state.offsetX = reset.offsetX;
state.offsetY = reset.offsetY;
state.panel = reset.panel;
state.origin = reset.origin;
state.moved = reset.moved;
return result;
}
function resolveOptionDisclosure(options, matchedIndexes, expanded) {
const all = options.map((o, i) => ({ o, i }));
const visible2 = expanded ? all : all.filter((option) => matchedIndexes.has(option.i));
return {
visible: visible2,
collapsible: expanded || visible2.length < all.length
};
}
const TYPE_LABELS = {
[QuestionType.Single]: "单选题",
[QuestionType.Multiple]: "多选题",
[QuestionType.Judge]: "判断题",
[QuestionType.Fill]: "填空题"
};
const harvestTypeLabel = (itemType) => {
const normalized = normalizeLeafQuestionType(itemType);
if (!normalized) return (itemType == null ? void 0 : itemType.trim()) || "题目";
return normalized === "short_answer" ? "简答题" : TYPE_LABELS[normalized];
};
const esc = (value) => value.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
const contentHtml = (content) => parseQuestionContent(content).map(
(part) => part.type === "image" ? `
` : esc(part.value)
).join("");
const letter = (index) => String.fromCharCode(65 + index);
const typeLabel = (it) => {
var _a2;
return ((_a2 = it.unit) == null ? void 0 : _a2.queryType) === "short_answer" ? "简答题" : TYPE_LABELS[it.q.type];
};
function buildPageExportHtml(items, meta) {
const sections = items.map((it, inx) => {
if (it.status === "decodeFail" || it.status === "unsupported") {
const reason = it.status === "decodeFail" ? "题面解析失败,已跳过" : "题目无合法文字或图片,已跳过";
return `第 ${inx + 1} 题
(${reason})
`;
}
const matched = it.unit ? new Set(answeredOptionIndexes(it.unit, it.answerPlan)) : /* @__PURE__ */ new Set();
const opts = it.q.options.map(
(option, i) => `${letter(i)}. ${contentHtml(option)}`
).join("");
const answer = it.answer.length ? `${it.answer.map(contentHtml).join(";")}${it.aiGenerated ? ' (AI 生成 · 待核对)' : ""}` : '未命中';
return `
第 ${inx + 1} 题 [${typeLabel(it)}]
${contentHtml(it.q.stem)}
${opts ? `${opts}
` : ""}
参考答案:${answer}
`;
}).join("\n");
return pageShell("本页题目与参考答案", meta, sections);
}
const harvestHitIndexes = (it) => {
const wanted = it.values.map(normalizeForMatch).filter((v) => v !== "");
const wantedTruth = it.values.map(normalizeTruth);
return new Set(
(it.options ?? []).flatMap((option, i) => {
const truth = normalizeTruth(option);
return wanted.includes(normalizeForMatch(option)) || truth !== null && wantedTruth.includes(truth) ? [i] : [];
})
);
};
function buildHarvestExportHtml(items, meta) {
const sections = items.map((it, inx) => {
const matched = harvestHitIndexes(it);
const opts = (it.options ?? []).map(
(option, i) => `${letter(i)}. ${contentHtml(option)}`
).join("");
return `
第 ${inx + 1} 题 [${esc(harvestTypeLabel(it.itemType))}]
${it.stem ? contentHtml(it.stem) : '(这条没有题面)'}
${opts ? `${opts}
` : ""}
答案:${it.values.map(contentHtml).join(";")}
`;
}).join("\n");
return pageShell("本页收录的题目与答案", meta, sections);
}
const pageShell = (heading, meta, sections) => `
爱问答 · ${esc(heading)}
${esc(heading)}
${esc(meta.platformLabel)} · ${esc(meta.exportedAt)}
${sections}
爱问答 · 答案仅供参考,自行核对。
`;
const _hoisted_1 = {
key: 0,
class: "tip"
};
const _hoisted_2 = {
key: 0,
class: "badge"
};
const _hoisted_3 = {
width: "0",
height: "0",
style: { "position": "absolute" },
"aria-hidden": "true"
};
const _hoisted_4 = ["aria-label"];
const _hoisted_5 = {
key: 1,
class: "ic",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "1.7",
"aria-hidden": "true"
};
const _hoisted_6 = { class: "tabbar" };
const _hoisted_7 = ["onClick"];
const _hoisted_8 = {
key: 0,
class: "subbar"
};
const _hoisted_9 = ["onClick"];
const _hoisted_10 = { class: "body" };
const _hoisted_11 = {
key: 0,
class: "card"
};
const _hoisted_12 = { class: "row" };
const _hoisted_13 = { class: "toolbar" };
const _hoisted_14 = ["title"];
const _hoisted_15 = {
key: 0,
class: "row"
};
const _hoisted_16 = { class: "mono cap-mute" };
const _hoisted_17 = {
key: 1,
class: "mono cap-mute"
};
const _hoisted_18 = {
key: 2,
class: "skip"
};
const _hoisted_19 = { class: "cap-mute" };
const _hoisted_20 = {
key: 1,
class: "standby"
};
const _hoisted_21 = { class: "cap-mute" };
const _hoisted_22 = {
key: 2,
class: "card"
};
const _hoisted_23 = { class: "row" };
const _hoisted_24 = { class: "tag neutral" };
const _hoisted_25 = { class: "gate-h" };
const _hoisted_26 = { class: "cap-mute" };
const _hoisted_27 = { class: "toolbar" };
const _hoisted_28 = { class: "grp" };
const _hoisted_29 = { class: "row" };
const _hoisted_30 = { class: "locator" };
const _hoisted_31 = { class: "toolbar" };
const _hoisted_32 = {
key: 0,
class: "tag acc"
};
const _hoisted_33 = { class: "cap-mute" };
const _hoisted_34 = {
key: 0,
class: "banner"
};
const _hoisted_35 = { class: "spacer" };
const _hoisted_36 = {
key: 1,
class: "card done"
};
const _hoisted_37 = { class: "prow" };
const _hoisted_38 = { class: "prow" };
const _hoisted_39 = { class: "prow" };
const _hoisted_40 = {
key: 0,
class: "prow"
};
const _hoisted_41 = {
key: 1,
class: "prow"
};
const _hoisted_42 = {
key: 2,
class: "prow"
};
const _hoisted_43 = {
key: 3,
class: "prow"
};
const _hoisted_44 = { class: "cap-mute" };
const _hoisted_45 = { class: "grp" };
const _hoisted_46 = {
key: 0,
class: "cap-mute"
};
const _hoisted_47 = {
key: 1,
class: "row"
};
const _hoisted_48 = { class: "tag acc" };
const _hoisted_49 = {
key: 2,
class: "cap-mute"
};
const _hoisted_50 = {
key: 2,
class: "grp"
};
const _hoisted_51 = { class: "grid" };
const _hoisted_52 = ["onClick"];
const _hoisted_53 = {
key: 3,
class: "card"
};
const _hoisted_54 = { class: "row" };
const _hoisted_55 = { class: "locator" };
const _hoisted_56 = { class: "row" };
const _hoisted_57 = { class: "locator" };
const _hoisted_58 = { class: "row question-head" };
const _hoisted_59 = { class: "locator" };
const _hoisted_60 = { class: "toolbar" };
const _hoisted_61 = {
key: 0,
class: "tag neutral"
};
const _hoisted_62 = ["disabled"];
const _hoisted_63 = { class: "stem" };
const _hoisted_64 = { class: "stem-type" };
const _hoisted_65 = { class: "opts" };
const _hoisted_66 = { class: "answer-block" };
const _hoisted_67 = { class: "row" };
const _hoisted_68 = { class: "toolbar" };
const _hoisted_69 = {
key: 0,
class: "tag neutral"
};
const _hoisted_70 = {
key: 1,
class: "tag neutral"
};
const _hoisted_71 = {
key: 0,
class: "answer-list"
};
const _hoisted_72 = { class: "answer-key" };
const _hoisted_73 = { class: "answer-value" };
const _hoisted_74 = { key: 0 };
const _hoisted_75 = {
key: 1,
class: "answer-item"
};
const _hoisted_76 = { class: "answer-value" };
const _hoisted_77 = {
key: 2,
class: "answer-value"
};
const _hoisted_78 = { key: 0 };
const _hoisted_79 = {
key: 3,
class: "cap-mute"
};
const _hoisted_80 = { class: "grp" };
const _hoisted_81 = { class: "row" };
const _hoisted_82 = { class: "toolbar" };
const _hoisted_83 = { class: "tag acc" };
const _hoisted_84 = { class: "ent-top" };
const _hoisted_85 = { class: "ent-ty" };
const _hoisted_86 = { class: "ent-tm mono" };
const _hoisted_87 = {
key: 0,
class: "cap-mute"
};
const _hoisted_88 = { class: "ent-a" };
const _hoisted_89 = {
key: 0,
class: "ent-ops"
};
const _hoisted_90 = { class: "cap-mute" };
const _hoisted_91 = { class: "standby" };
const _hoisted_92 = { class: "cap-mute" };
const _hoisted_93 = {
key: 0,
class: "grp"
};
const _hoisted_94 = { class: "row" };
const _hoisted_95 = { class: "mono cap-mute" };
const _hoisted_96 = { class: "switch-row" };
const _hoisted_97 = ["onClick", "aria-label"];
const _hoisted_98 = {
class: "lbl",
style: { "flex": "1" }
};
const _hoisted_99 = { class: "cap-mute" };
const _hoisted_100 = { class: "row" };
const _hoisted_101 = { class: "mono cap-mute" };
const _hoisted_102 = { class: "grp" };
const _hoisted_103 = { class: "switch-row" };
const _hoisted_104 = { class: "grp" };
const _hoisted_105 = { class: "row" };
const _hoisted_106 = { class: "cap-mute" };
const _hoisted_107 = {
key: 0,
class: "alert"
};
const _hoisted_108 = {
key: 1,
class: "alert"
};
const _hoisted_109 = { class: "row" };
const _hoisted_110 = ["disabled"];
const _hoisted_111 = {
key: 2,
class: "cap-mute"
};
const _hoisted_112 = { class: "grp" };
const _hoisted_113 = { class: "switch-row" };
const _hoisted_114 = { class: "row" };
const _hoisted_115 = { class: "mono cap-mute" };
const _hoisted_116 = { class: "row" };
const _hoisted_117 = { class: "cap-mute" };
const _hoisted_118 = { class: "grp" };
const _hoisted_119 = ["onClick", "aria-label"];
const _hoisted_120 = {
class: "lbl",
style: { "flex": "1" }
};
const _hoisted_121 = { class: "prev" };
const _hoisted_122 = { class: "prow" };
const _hoisted_123 = { class: "prow" };
const _hoisted_124 = { class: "prow" };
const _hoisted_125 = { class: "prow" };
const _hoisted_126 = {
key: 0,
class: "alert"
};
const _hoisted_127 = { class: "prev" };
const _hoisted_128 = { class: "prow" };
const _hoisted_129 = { class: "toolbar" };
const _hoisted_130 = { class: "cap-mute mono" };
const _hoisted_131 = { class: "meter" };
const _hoisted_132 = {
key: 0,
class: "alert"
};
const _hoisted_133 = {
key: 1,
class: "alert"
};
const _hoisted_134 = {
key: 2,
class: "cap-mute"
};
const _hoisted_135 = {
key: 0,
class: "alert"
};
const _hoisted_136 = {
key: 1,
class: "cap-mute"
};
const _hoisted_137 = {
key: 4,
class: "cap-mute"
};
const _hoisted_138 = {
key: 5,
class: "cap-mute"
};
const _hoisted_139 = { class: "ent-top" };
const _hoisted_140 = { class: "ent-ty" };
const _hoisted_141 = {
key: 0,
class: "ent-ty"
};
const _hoisted_142 = {
key: 1,
class: "ent-ty"
};
const _hoisted_143 = { class: "ent-tm" };
const _hoisted_144 = ["aria-label", "onClick"];
const _hoisted_145 = { class: "ent-a" };
const _hoisted_146 = {
key: 0,
class: "ent-ops"
};
const _hoisted_147 = { class: "cap-mute" };
const _hoisted_148 = {
key: 6,
class: "cap-mute"
};
const _hoisted_149 = { class: "statcard" };
const _hoisted_150 = { class: "row" };
const _hoisted_151 = {
key: 0,
class: "statgrid"
};
const _hoisted_152 = { key: 0 };
const _hoisted_153 = { key: 1 };
const _hoisted_154 = {
key: 1,
class: "cap-mute"
};
const _hoisted_155 = {
key: 2,
class: "alert"
};
const _hoisted_156 = { class: "grp" };
const _hoisted_157 = { class: "log-filter" };
const _hoisted_158 = ["onClick"];
const _hoisted_159 = {
key: 0,
class: "log-list"
};
const _hoisted_160 = { class: "log-time mono" };
const _hoisted_161 = { class: "log-msg" };
const _hoisted_162 = {
key: 0,
class: "log-repeat mono"
};
const _hoisted_163 = {
key: 1,
class: "cap-mute"
};
const _hoisted_164 = { class: "grp" };
const _hoisted_165 = ["disabled"];
const _hoisted_166 = {
key: 0,
class: "cap-mute"
};
const _hoisted_167 = {
key: 1,
class: "cap-mute"
};
const _hoisted_168 = {
key: 1,
class: "rule-meta"
};
const _hoisted_169 = { class: "rule-row" };
const _hoisted_170 = { class: "rule-value" };
const _hoisted_171 = {
key: 0,
class: "rule-row"
};
const _hoisted_172 = { class: "rule-value" };
const _hoisted_173 = {
key: 1,
class: "rule-row"
};
const _hoisted_174 = { class: "rule-value" };
const _hoisted_175 = {
key: 2,
class: "rule-row"
};
const _hoisted_176 = { class: "rule-value" };
const _hoisted_177 = {
key: 2,
class: "cap-mute"
};
const _hoisted_178 = { class: "actbar" };
const _hoisted_179 = {
key: 0,
class: "prev"
};
const _hoisted_180 = { class: "prow" };
const _hoisted_181 = { class: "prow" };
const _hoisted_182 = ["disabled"];
const _hoisted_183 = ["disabled"];
const _hoisted_184 = {
key: 0,
class: "prog"
};
const _hoisted_185 = { class: "stat" };
const _hoisted_186 = { class: "ticks" };
const _hoisted_187 = {
key: 1,
class: "toolbar"
};
const _hoisted_188 = ["disabled"];
const _hoisted_189 = {
key: 0,
class: "toolbar"
};
const _hoisted_190 = {
key: 1,
class: "toolbar"
};
const _hoisted_191 = {
key: 2,
class: "toolbar"
};
const _hoisted_192 = ["disabled"];
const _hoisted_193 = { class: "actbar-foot" };
const _hoisted_194 = { class: "cap-mute mono" };
const _hoisted_195 = {
key: 2,
class: "pop"
};
const _hoisted_196 = { class: "toolbar" };
const _hoisted_197 = ["disabled"];
const _hoisted_198 = ["disabled"];
const _hoisted_199 = {
key: 0,
class: "cap-mute"
};
const _hoisted_200 = { class: "home-user" };
const _hoisted_201 = { class: "ava lg" };
const _hoisted_202 = { class: "home-meta" };
const _hoisted_203 = { class: "ctitle" };
const _hoisted_204 = {
key: 0,
class: "cap-mute"
};
const _hoisted_205 = ["disabled"];
const _hoisted_206 = {
key: 0,
class: "cap-mute"
};
const _hoisted_207 = { class: "row" };
const _hoisted_208 = {
key: 0,
class: "toolbar"
};
const _hoisted_209 = { class: "balance" };
const _hoisted_210 = {
key: 1,
class: "cap-mute"
};
const _hoisted_211 = {
key: 1,
class: "cap-mute"
};
const _hoisted_212 = { class: "toolbar" };
const _hoisted_213 = ["disabled"];
const _hoisted_214 = {
key: 2,
class: "cap-mute"
};
const _hoisted_215 = { class: "row sep-top" };
const _hoisted_216 = { class: "cap-mute" };
const _hoisted_217 = {
key: 3,
class: "captcha-cover",
role: "dialog",
"aria-modal": "true",
"aria-label": "完成注册人机验证"
};
const _hoisted_218 = { class: "captcha-card" };
const AUTH_STALE_NOTE = "登录未通过验证 · 换过浏览器或重装脚本需重登一次";
const SUBMIT_ACCEPTED_NOTE = "已点确认 · 站点没报错。卷面要等页面刷新才转态,下次翻到本节会自动复核。";
const ANSWERING_EMPTY_TICKS = 10;
const CACHE_LIST_LIMIT = 200;
const HARVEST_RECHECK_MS = 3e4;
const _sfc_main = /* @__PURE__ */ vue.defineComponent({
__name: "Panel",
setup(__props) {
var _a2;
const IS_DEV = false;
const collapsed = vue.ref(getCollapsed());
const expand = () => {
switchPanel(false);
};
const collapse = () => {
switchPanel(true);
};
const panelRef = vue.ref(null);
const dragHandleRef = vue.ref(null);
const dragState = vue.reactive(createPanelDragState());
const launcherGesture = vue.reactive(createPanelLauncherGestureState());
const pos = vue.ref(getPanelPosition());
let panelResizeObserver = null;
let pendingPanelResize = null;
const panelStyle = vue.computed(
() => pos.value ? {
left: `${pos.value.x}px`,
top: `${pos.value.y}px`,
right: "auto",
bottom: "auto"
} : {}
);
function readViewportSize() {
const el = document.documentElement;
const width = el.clientWidth;
const height = el.clientHeight;
if (width === 0 || height === 0) {
return { width: window.innerWidth, height: window.innerHeight };
}
return { width, height };
}
function switchPanel(nextCollapsed) {
var _a3;
if (collapsed.value === nextCollapsed) return;
const rect = (_a3 = panelRef.value) == null ? void 0 : _a3.getBoundingClientRect();
pendingPanelResize = pos.value && rect ? {
position: { x: rect.left, y: rect.top },
panel: { width: rect.width, height: rect.height }
} : null;
collapsed.value = nextCollapsed;
setCollapsed(nextCollapsed);
}
function reconcilePanelPosition(options = {}) {
if (pos.value === null) return;
const panelEl = panelRef.value;
if (!panelEl) return;
if (dragState.pointerId !== null) return;
const rect = panelEl.getBoundingClientRect();
const viewport = readViewportSize();
const next = clampPanelPosition(
pos.value,
{ width: rect.width, height: rect.height },
viewport
);
const changed = next.x !== pos.value.x || next.y !== pos.value.y;
if (changed) pos.value = next;
if (options.persist || changed) setPanelPosition(pos.value);
}
function beginDrag(event, interactive) {
const panelEl = panelRef.value;
const handle = event.currentTarget instanceof HTMLElement ? event.currentTarget : dragHandleRef.value;
if (!panelEl || !handle) return;
const rect = panelEl.getBoundingClientRect();
const ok = beginPanelDrag(dragState, {
pointerId: event.pointerId,
isPrimary: event.isPrimary,
pointerType: event.pointerType,
button: event.button,
clientX: event.clientX,
clientY: event.clientY,
rect: {
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height
},
interactive
});
if (!ok) return;
event.preventDefault();
try {
handle.setPointerCapture(event.pointerId);
} catch {
endPanelDrag(dragState, event.pointerId);
}
}
function startDrag(event) {
beginDrag(event, isPanelDragInteractiveTarget(event.target));
}
function startBubbleDrag(event) {
beginPanelLauncherGesture(launcherGesture);
beginDrag(event, false);
}
function moveDrag(event) {
const next = movePanelDrag(
dragState,
{
pointerId: event.pointerId,
clientX: event.clientX,
clientY: event.clientY
},
readViewportSize()
);
if (next) pos.value = next;
}
function finishDrag(event) {
var _a3;
const result = endPanelDrag(dragState, event.pointerId);
const handle = event.currentTarget instanceof HTMLElement ? event.currentTarget : dragHandleRef.value;
if ((_a3 = handle == null ? void 0 : handle.hasPointerCapture) == null ? void 0 : _a3.call(handle, event.pointerId)) {
try {
handle.releasePointerCapture(event.pointerId);
} catch {
}
}
if (!result) return;
if (collapsed.value && endPanelLauncherGesture(launcherGesture, result, event.type)) {
expand();
}
reconcilePanelPosition({ persist: true });
}
function activateLauncher(event) {
if (!consumePanelLauncherActivation(launcherGesture, event.detail)) {
event.preventDefault();
return;
}
expand();
}
vue.watch(
panelRef,
(next, previous) => {
if (previous && panelResizeObserver) panelResizeObserver.unobserve(previous);
if (!next) return;
if (pendingPanelResize) {
const rect = next.getBoundingClientRect();
pos.value = remapPanelPosition(
pendingPanelResize.position,
pendingPanelResize.panel,
{ width: rect.width, height: rect.height },
readViewportSize()
);
pendingPanelResize = null;
setPanelPosition(pos.value);
}
reconcilePanelPosition();
if (typeof ResizeObserver === "undefined") return;
if (!panelResizeObserver) {
panelResizeObserver = new ResizeObserver(() => {
reconcilePanelPosition();
});
}
panelResizeObserver.observe(next);
},
{ flush: "post" }
);
function onPanelKeydown(e) {
if (e.key === "F9") {
switchPanel(!collapsed.value);
}
}
function onWindowResize() {
reconcilePanelPosition();
}
function onPageHide() {
localAnswerCache.flush();
}
const loggedIn = vue.ref(!!getToken());
const authStale = vue.ref(false);
const SUBMIT_SKIP_REASON = {
"clicked-entry": "点开了提交,但没等到站点的确认框",
"confirm-unverified": "确认框没关或站点报了错,卷子多半没交出去",
"below-threshold": "未达阈值,只暂存",
// 与上一条分开说:这一档调阈值滑杆没用,卷子上有题根本没被识别出来。
"unrecognized-questions": "卷面上有题没被识别出来,只暂存 · 调阈值解决不了,题型要先补进规则",
"exam-page": "考试页永不自动交卷",
"no-entry": "本页没有可识别的提交入口",
"click-failed": "提交入口点不动",
"site-quota": "站点这次加载的提交次数已用完 · 刷新页面后可再交",
"site-locked": "站点这份卷子已在提交中或已交过 · 刷新页面后可再交",
"no-page-window": "这一刻读不到答题帧(多半正在重载)· 下一轮再试"
};
function countAnswerable() {
const selector = courseConfig().chapterTestAnswerable;
let total = 0;
for (const doc of readableDocuments(document)) {
try {
total += doc.querySelectorAll(selector).length;
} catch {
}
}
return total > 0 ? total : void 0;
}
const submitOutcome = vue.ref(null);
const submitNote = vue.computed(() => {
const outcome = submitOutcome.value;
if (outcome === "submitted") return "已提交 · 卷面已转为已完成。";
if (outcome === "confirm-accepted") return SUBMIT_ACCEPTED_NOTE;
if (!outcome || outcome === "off" || outcome === "no-items")
return "页面未提交,可自行检查后再交。";
return `未提交 · ${SUBMIT_SKIP_REASON[outcome]}。已填的部分已暂存。`;
});
const tab = vue.ref("home");
const accountOpen = vue.ref(false);
const toggleAccount = () => {
accountOpen.value = !accountOpen.value;
if (accountOpen.value) void refreshMe();
};
const closeAccount = () => {
accountOpen.value = false;
};
const avatarInitial = vue.computed(
() => accountName.value ? [...accountName.value][0].toUpperCase() : ""
);
const systemSub = vue.ref("general");
const goCacheManage = () => {
tab.value = "system";
systemSub.value = "cache";
};
const TABS = [
{ k: "home", l: "首页" },
{ k: "ask", l: "答题" },
{ k: "harvest", l: "收录" },
{ k: "system", l: "系统" }
];
const SYSTEM_SEGS = [
{ k: "general", l: "通用" },
// 用户可见的说法一律「课程学习」——超星自己的页面就叫这个名字,OCS 的同名脚本
// 也是。内部标识符(course-*/course-automation)本来就是中性的,不必跟着改。
{ k: "course", l: "课程", feature: "course-automation" },
{ k: "cache", l: "缓存" },
{ k: "diag", l: "诊断" }
];
const QUESTION_TYPE_LABELS = {
[QuestionType.Single]: "单选",
[QuestionType.Multiple]: "多选",
[QuestionType.Judge]: "判断",
[QuestionType.Fill]: "填空"
};
const accountName = vue.ref(getUsername());
const username = vue.ref("");
const password = vue.ref("");
const email = vue.ref("");
const authMsg = vue.ref("");
const authing = vue.ref(false);
const captchaOpen = vue.ref(false);
const captchaFrame = vue.ref(null);
const captchaState = vue.ref("");
const captchaUrl = `${BACKEND_BASE_URL}/captcha`;
let captchaRequest = null;
let captchaPending = null;
function finishCaptcha(error, token) {
const pending = captchaPending;
captchaPending = null;
captchaRequest = null;
captchaOpen.value = false;
captchaState.value = "";
if (!pending) return;
if (error || !token) pending.reject(error ?? new Error("challenge-failed"));
else pending.resolve(token);
}
function requestRegistrationCaptcha() {
if (captchaPending) return Promise.reject(new Error("challenge-busy"));
captchaState.value = crypto.randomUUID();
captchaOpen.value = true;
return new Promise((resolve, reject) => {
captchaPending = { resolve, reject };
});
}
function onCaptchaFrameLoad() {
var _a3;
if (captchaRequest || !captchaPending || !captchaState.value) return;
const frameWindow = (_a3 = captchaFrame.value) == null ? void 0 : _a3.contentWindow;
if (!frameWindow) {
finishCaptcha(new Error("challenge-unavailable"));
return;
}
captchaRequest = createCaptchaFrameRequest({
frameWindow,
targetOrigin: new URL(BACKEND_BASE_URL).origin,
state: captchaState.value,
timeoutMs: 18e4
});
void captchaRequest.result.then(
(token) => finishCaptcha(null, token),
(error) => finishCaptcha(
error instanceof Error ? error : new Error("challenge-failed")
)
);
}
function cancelCaptcha() {
if (captchaRequest) captchaRequest.cancel();
else finishCaptcha(new Error("cancelled"));
}
async function doAuth(mode) {
authing.value = true;
authMsg.value = "";
let captchaToken;
if (mode === "register") {
try {
captchaToken = await requestRegistrationCaptcha();
} catch {
authMsg.value = "人机验证未完成,可重试。";
authing.value = false;
return;
}
}
const r = await authenticate(
aiaskTransport,
mode,
username.value.trim(),
password.value,
BACKEND_BASE_URL,
captchaToken,
email.value
);
if (r.token) {
setToken(r.token);
setUsername(username.value.trim());
accountName.value = username.value.trim();
loggedIn.value = true;
authStale.value = false;
password.value = "";
email.value = "";
tab.value = loaded && list.value.length > 0 ? "ask" : "home";
pushLog(mode === "register" ? "注册成功" : "登录成功", "info");
void refreshMe().then(() => {
if (mode === "register" && balance.value != null)
pushLog(`已送 ${balance.value} 分,可以直接开始答题`, "info");
});
if (noteAction.value === "login") {
note2.value = "";
noteAction.value = "";
}
} else authMsg.value = r.message;
authing.value = false;
}
async function devAutoLogin() {
return;
}
const markAuthStale = () => {
authStale.value = true;
if (!username.value) username.value = accountName.value;
};
const logout = () => {
clearToken();
clearLastBalance();
balance.value = null;
loggedIn.value = false;
authStale.value = false;
username.value = accountName.value;
pushLog("已退出登录", "info");
discard();
};
const cardCode = vue.ref("");
const redeemNote = vue.ref("");
const redeeming = vue.ref(false);
const balance = vue.ref(getLastBalance());
const emailBound = vue.ref(null);
async function refreshMe() {
if (!getToken()) return;
const snapshot2 = await fetchMe(aiaskTransport, BACKEND_BASE_URL);
if (!snapshot2) return;
balance.value = snapshot2.balance;
setLastBalance(snapshot2.balance);
accountName.value = snapshot2.username;
emailBound.value = snapshot2.emailBound;
authStale.value = false;
}
async function doRedeem() {
const code = cardCode.value.trim();
if (!code || redeeming.value) return;
const token = getToken();
if (!token) {
redeemNote.value = "需先登录。";
return;
}
redeeming.value = true;
redeemNote.value = "";
const r = await redeemCard(aiaskTransport, code, BACKEND_BASE_URL);
if (typeof r.balance === "number") {
balance.value = r.balance;
setLastBalance(r.balance);
authStale.value = false;
session == null ? void 0 : session.resumePaidAfterCredit();
cardCode.value = "";
redeemNote.value = `兑换成功 · 余额 ${r.balance} 分`;
if (noteAction.value === "account") {
note2.value = "";
noteAction.value = "";
}
pushLog(`卡密兑换成功 · 余额 ${r.balance}`, "info");
} else {
redeemNote.value = r.message;
pushLog(`卡密兑换失败 · ${r.message}`, "warning");
if (r.unauthorized) {
markAuthStale();
note2.value = AUTH_STALE_NOTE;
noteAction.value = "login";
}
}
redeeming.value = false;
}
const settings = vue.reactive(getSettings());
const persist = () => setSettings({
autoFill: true,
delayMs: settings.delayMs,
reportHealth: settings.reportHealth,
freeFirst: settings.freeFirst,
courseAuto: settings.courseAuto,
coursePlaybackRate: settings.coursePlaybackRate,
// 拷一份再存:reactive 代理直接塞进 GM_setValue 会连同代理一起序列化,
// 而这是个嵌套对象,逐字段列举反而更容易漏掉新加的类型。
courseTaskToggles: { ...settings.courseTaskToggles },
autoStart: settings.autoStart,
autoSubmit: settings.autoSubmit,
autoSubmitThreshold: settings.autoSubmitThreshold,
randomFallback: settings.randomFallback
});
const makeToggle = (key, msg, after) => () => {
settings[key] = !settings[key];
persist();
after == null ? void 0 : after();
if (msg)
pushLog(
settings[key] ? msg.on : msg.off,
settings[key] && msg.warnOn ? "warning" : "info"
);
};
const toggleReport = makeToggle("reportHealth");
let mediaRunner = null;
const mediaState = vue.ref(null);
const MEDIA_STATE_TEXT = {
idle: "本页没有可播放的任务点",
loading: "内容加载中 · 等它就绪",
playing: "正在播放",
finished: "本任务点已办完",
reading: "正在阅读文档任务点",
advancing: "切到下一个任务点",
"advancing-section": "本节过完 · 切下一节",
"section-done": "本节过完 · 没有下一节",
"all-done": "本节任务点已全部完成",
"course-done": "全部章节任务点已完成",
hyperlink: "已点开链接任务点",
starting: "已点开播放器 · 等它起播",
"advance-failed": "切下一节没生效 · 手动翻页后再打开",
"face-recognition": "出现人脸识别 · 你识别完自动接着播",
"media-error": "播放器报错 · 已停下等你处理",
"video-quiz": "视频里弹出题目 · 你答完自动接着播",
"not-playing": "没能自动播起来 · 手动点一下播放器",
locked: "闯关模式卡住 · 先手动完成前置任务点",
"budget-exhausted": "已达单节时长上限 · 已停止"
};
const mediaStatusText = vue.computed(() => {
const state = mediaState.value;
if (!state) return "未开启";
if (state.kind === "reading")
return `正在阅读文档任务点 · ${state.summary.frames} 帧 / 拉到底 ${state.summary.scrolled} 处`;
if (state.kind === "dwelling")
return `长时阅读驻留 · 还剩 ${Math.ceil(state.remainingMs / 1e3)} 秒`;
if (state.kind === "ppt-slide") return `课件翻页中 · 共 ${state.total} 张`;
if (state.kind === "answering" && !state.frameLoaded)
return "章节测验在另一个任务点上 · 正在切过去";
if (state.kind === "answering")
return list.value.length > 0 ? `轮到章节测验 · 答题引擎已接手 ${list.value.length} 题` : "轮到章节测验 · 答题引擎还没识别到题目";
if (state.kind === "advancing-chapter") return `切到下一章 · ${state.name}`;
if (state.kind === "section-stalled")
return `本节还剩 ${state.unfinished} 个任务点站点没认 · 能做的都做了`;
return MEDIA_STATE_TEXT[state.kind === "blocked" ? state.reason : state.kind] ?? "未知状态";
});
const courseProgress2 = vue.ref(null);
const courseStatusText = vue.computed(
// course-card.ts 里 courseStatusLine 对没有特化的状态返回空串,
// 这里用 `||` 接住那个哨兵值,退回原有的 mediaStatusText。
() => courseStatusLine(mediaState.value, courseProgress2.value) || mediaStatusText.value
);
const coursePositionText = vue.computed(
() => coursePositionLine(courseProgress2.value)
);
const courseCountText = vue.computed(() => courseCountLine(courseProgress2.value));
const courseSkipped = vue.computed(
() => {
var _a3, _b;
return ((_b = (_a3 = courseProgress2.value) == null ? void 0 : _a3.section) == null ? void 0 : _b.skipped) ?? [];
}
);
const legacyCourseUrl = legacyStudentstudyUpgradeUrl(location);
const onCourseStudyPage = isNewCourseStudyUrl(location);
const switchToNewCoursePage = () => {
if (legacyCourseUrl) location.href = legacyCourseUrl;
};
let answeringTask = "";
let answeringTicksSeen = 0;
function onAnsweringTick(state) {
if (state.taskKey !== answeringTask) {
answeringTask = state.taskKey;
answeringTicksSeen = 0;
roundStarted.value = false;
}
answeringTicksSeen = state.ticks ?? 0;
if (running.value) return;
if (state.ticks === ANSWERING_TICKS_BUDGET) {
pushLog(
`让路窗口已用完 · ${state.name} 等不到可答的题目 · 已跳过,继续后续任务`,
"warning"
);
return;
}
if (!state.frameLoaded || loaded) return;
if (state.ticks === ANSWERING_EMPTY_TICKS)
pushLog(
`${state.name} 没有本脚本能答的题(题型可能不支持)· 已跳过,继续后续任务`,
"warning"
);
pageChangeScheduler == null ? void 0 : pageChangeScheduler.notify();
}
const syncMediaTask = () => {
if (!settings.courseAuto || !hasFeature("course-automation") || !onCourseStudyPage || !courseAdapter) {
mediaRunner == null ? void 0 : mediaRunner.stop();
mediaRunner = null;
mediaState.value = null;
courseProgress2.value = null;
pauseCourseMedia(document);
return;
}
if (mediaRunner) return;
syncCourseConfig();
mediaRunner = runMediaTask(document, {
// 上面闸已挡掉 courseAdapter 为 null 的情形,这里必非 null。
adapter: courseAdapter,
/**
* 内存护栏:越线就整页导航,理由与真页数据见 `course/memory-guard.ts` 文件头。
* 一句话——泄漏在超星的 UEditor 上,我们改不了它,只能不喂它。
*/
memoryGuard: createMemoryGuard({ sample: readUsedJsHeap(window) }),
onMemoryPressure: (usedBytes) => {
const mb = Math.round(usedBytes / 1048576);
pushLog(
`内存占用 ${mb} MB · 整页刷新后自动继续(超星章测页的已知泄漏)`,
"warning"
);
window.location.reload();
},
// getter 而非定值:倍速滑杆拖动后当拍生效,不必关掉再开。
get playbackRate() {
return settings.coursePlaybackRate;
},
// 同理用函数读:设置里勾掉某一类,当拍生效,不必关掉刷课再开。
// 分类 → 开关。`unknown` 不归任何开关管,永不可关:站点声明了却没人认得的
// 任务点,正是最该做、也最该被如实报出来的那种。
// 答完这一轮就放行,别让低于阈值的卷子把队列白锁三分钟(2026-08-19 真机)。
// 答完就放行;识别不到题的卷子等够宽限期也放行(见 ANSWERING_EMPTY_TICKS)。
// **先认任务点**:这一问在 onState 之前,key 对不上说明问的是我们还没看过的
// 那一份卷子,此刻手里的读数全是上一份的,一律不放行(见 onAnsweringTick)。
isAnsweringDone: (taskKey) => taskKey === answeringTask && (runDone.value || !loaded && answeringTicksSeen >= ANSWERING_EMPTY_TICKS),
isKindEnabled: (kind) => {
const key = toggleForKind(kind);
return key === null || settings.courseTaskToggles[key] !== false;
},
// 每换一节报一次「我看见了什么」。真机上引擎曾一路报「本节过完」飞掠整门课,
// 而站点数据一次都没读到——状态行看不出走的是站点事实源还是 DOM 兜底,只能靠猜。
onSurvey: (report) => {
pushLog(
`本节盘点 · ${report.frames} 帧 · 站点数据${report.authoritative ? `已读到 ${report.declared} 个任务点` : "未读到"} · 认出 ${report.kinds.length} 个(${report.kinds.join("、") || "无"})· 待办 ${report.pending}`,
report.authoritative ? "info" : "warning"
);
for (const item of report.skipped) {
const label = item.name === KIND_LABEL[item.kind] ? KIND_LABEL[item.kind] : `${KIND_LABEL[item.kind]}「${item.name}」`;
pushLog(
`跳过 ${label} · ${item.reason === "media-ended" ? "本页媒体已播完" : TASK_SKIP_LABEL[item.reason]}`,
"info"
);
}
},
onState: (state) => {
mediaState.value = state;
if (state.kind === "answering") onAnsweringTick(state);
if (isRunnerStopped(state)) courseProgress2.value = null;
if (state.kind === "reading")
pushLog(
`文档任务点 · 扫到 ${state.summary.frames} 帧 · 拉到底 ${state.summary.scrolled} 处 · 翻页 ${state.summary.pagers} 次`,
state.summary.scrolled || state.summary.pagers ? "info" : "warning"
);
if (state.kind === "section-stalled")
pushLog(
`本节仍有 ${state.unfinished} 个任务点未被站点认可 · ${state.names.join("、")}`,
"warning"
);
if (state.kind === "course-done")
pushLog("侧栏所有章节的未完成计数已归零", "info");
},
// 旁路快照:只喂给卡片渲染,不参与判定与推进逻辑。
onProgress: (progress) => {
courseProgress2.value = progress;
}
});
};
const GENERAL_SWITCHES = [
{
key: "freeFirst",
label: "免费题库优先",
hint: "先查免费源,未命中再查付费源",
toggle: makeToggle("freeFirst")
},
{
key: "autoStart",
label: "检测到题目自动开始答题",
hint: "命中付费题库才扣分。关着时检测到题目只切到答题页,等你按「开始答题」。",
toggle: makeToggle("autoStart", {
on: "已开启检测到题目自动开始答题",
off: "已关闭自动开始答题",
warnOn: true
})
},
{
key: "randomFallback",
label: "无答案时随机作答",
hint: "仅单选与判断,其余题型留空。随机答案不进本地缓存,也不算提交阈值里的可信命中。",
toggle: makeToggle("randomFallback", {
on: "已开启无答案随机作答 · 仅单选与判断",
off: "已关闭随机作答",
warnOn: true
})
},
{
key: "autoSubmit",
label: "整卷答完自动提交",
hint: "考试页永不自动交卷——交卷撤不回来。",
toggle: makeToggle("autoSubmit", {
on: "已开启整卷答完自动提交 · 考试页除外",
off: "已关闭自动提交",
warnOn: true
})
}
];
const taskToggles = TASK_TOGGLES;
const taskToggleLabel = TOGGLE_LABEL;
const toggleTaskKind = (key) => {
settings.courseTaskToggles[key] = !settings.courseTaskToggles[key];
persist();
pushLog(
`${TOGGLE_LABEL[key]}任务点已${settings.courseTaskToggles[key] ? "开启" : "关闭"}`,
"info"
);
};
const toggleCourseAuto = makeToggle(
"courseAuto",
{ on: "已开启任务点自动播放", off: "已关闭任务点自动播放" },
syncMediaTask
);
function openCourseSettings() {
tab.value = "system";
systemSub.value = "course";
}
const PLAYBACK_RATES = [1, 1.5, 2];
function cyclePlaybackRate() {
const index = PLAYBACK_RATES.indexOf(
settings.coursePlaybackRate
);
settings.coursePlaybackRate = PLAYBACK_RATES[(index + 1) % PLAYBACK_RATES.length] ?? 1;
persist();
}
function skipReasonLabel(reason) {
if (reason === "media-ended") return "本页媒体已播完";
return TASK_SKIP_LABEL[reason];
}
const localCacheCount = vue.ref(localAnswerCache.size());
const cachePersistFailed = vue.ref(localAnswerCache.hasPersistFailure());
const syncCacheCount = () => {
localCacheCount.value = localAnswerCache.size();
const failed2 = localAnswerCache.hasPersistFailure();
if (failed2 && !cachePersistFailed.value)
pushLog("本地缓存写入失败 · 最近的收录可能没有落盘", "warning");
cachePersistFailed.value = failed2;
};
const cacheNearWarn = vue.computed(
() => localCacheCount.value >= CACHE_WARN_ENTRIES * 0.8
);
const cacheOverWarn = vue.computed(() => localCacheCount.value > CACHE_WARN_ENTRIES);
const harvestedCount = vue.ref(0);
const harvestedList = vue.ref([]);
const cacheEntries = vue.ref(localAnswerCache.list());
const cacheQuery = vue.ref("");
const cacheImportPreview = vue.ref(null);
const pendingImportText = vue.ref("");
const cacheNote = vue.ref("");
const cacheClearPending = vue.ref(false);
const refreshCache = () => {
cacheEntries.value = localAnswerCache.list();
syncCacheCount();
};
const importedNeverHit = vue.computed(() => {
const imported = cacheEntries.value.filter((e) => e.importedAt > 0);
return {
total: imported.length,
neverHit: imported.filter((e) => !e.lastHitAt).length
};
});
const matchedCache = vue.computed(() => {
const q = cacheQuery.value.trim().toLowerCase();
if (!q) return cacheEntries.value;
return cacheEntries.value.filter(
(e) => e.stem.toLowerCase().includes(q) || e.values.join(" ").toLowerCase().includes(q) || e.options.join(" ").toLowerCase().includes(q)
);
});
const filteredCache = vue.computed(
() => matchedCache.value.slice(0, CACHE_LIST_LIMIT)
);
const removeCacheEntry = (unitHash) => {
localAnswerCache.remove(unitHash);
refreshCache();
};
const exportCache = () => {
downloadText(`aiask-cache-${Date.now()}.json`, localAnswerCache.exportJson());
};
const PARSE_IMPORT_URL = `${IMPORT_BRIDGE_ORIGIN}${IMPORT_BRIDGE_PATHNAME}`;
const pickImportFile = () => {
const input = document.createElement("input");
input.type = "file";
input.accept = "application/json";
input.onchange = async () => {
var _a3;
const file = (_a3 = input.files) == null ? void 0 : _a3[0];
if (!file) return;
try {
const text = await file.text();
cacheImportPreview.value = localAnswerCache.previewImport(text);
pendingImportText.value = text;
cacheNote.value = "";
} catch {
cacheImportPreview.value = null;
pendingImportText.value = "";
cacheNote.value = "读不出这个文件,它需要是爱问答导出的 JSON。";
}
};
input.click();
};
const confirmImport = () => {
const text = pendingImportText.value;
if (!text) return;
let fresh = null;
try {
fresh = localAnswerCache.previewImport(text);
} catch {
cacheImportPreview.value = null;
pendingImportText.value = "";
cacheNote.value = "导入失败,缓存未改动。";
return;
}
const stale = cacheImportPreview.value;
if (!stale || fresh.added !== stale.added || fresh.replaced !== stale.replaced || fresh.total !== stale.total) {
cacheImportPreview.value = fresh;
cacheNote.value = "缓存在这期间有变化,数字已更新,确认后再导入。";
return;
}
try {
const result = localAnswerCache.importJson(text);
cacheNote.value = `已导入 ${result.added + result.replaced} 条。`;
} catch {
cacheNote.value = "导入失败,缓存未改动。";
}
cacheImportPreview.value = null;
pendingImportText.value = "";
refreshCache();
};
const cancelImport = () => {
cacheImportPreview.value = null;
pendingImportText.value = "";
};
const clearCacheAll = () => {
localAnswerCache.clear();
cacheQuery.value = "";
cacheClearPending.value = false;
refreshCache();
cacheNote.value = "已清空本地缓存。";
};
const cacheDate = (savedAt) => {
if (!savedAt) return "";
const d = new Date(savedAt);
const pad = (n) => String(n).padStart(2, "0");
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
};
const logBuf = createLogBuffer(200);
const logs = vue.ref([]);
const logFilter = vue.ref("all");
const LOG_LEVELS = [
{ k: "all", l: "全部" },
{ k: "info", l: "信息" },
{ k: "warning", l: "警告" },
{ k: "error", l: "错误" }
];
const filteredLogs = vue.computed(() => filterLogs(logs.value, logFilter.value));
function pushLog(content, type = "info") {
logBuf.add(content, type);
logs.value = logBuf.list().map((entry) => ({ ...entry }));
}
const clearLogs = () => {
logBuf.clear();
logs.value = [];
};
const downloadText = (filename, text, type = "application/json") => {
const url = URL.createObjectURL(new Blob([text], { type }));
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
};
const exportDiagnostics = () => {
const payload = {
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
version: SCRIPT_VERSION,
rule: ruleDiag.value ?? null,
/**
* 规则在 capture 阶段自己挂了的失败码。**null 不等于「这页真的没题」**——
* 逐条解码失败会被捕获里的逐条 try/catch 吞掉,流程照样干净返回 0(#165)。
* 它以前只喂给健康上报、不进导出,于是排障要绕到服务端查
* `rule_failure_client_day` 才看得见(#166)。
*
* 读的是面板正在显示的那份,不是现从 adapter 上取——排障文件和用户看见的屏幕
* 必须是同一句话,否则「他说有告警,文件里却是 null」会再制造一轮误判。
*/
ruleCaptureFailure: lastCaptureFailure.value,
// 超星字体表可用性。非 ok 时带混淆字体的题一条都抓不到,而症状只是「没识别到题目」
// ——#165 的根因。挂载时那条告警会被 200 条日志环冲掉,所以这里独立记一份。
fontTable: chaoxingFontTableStatus(),
/**
* 奥鹏接口旁听 hook 的安装状态,非奥鹏站点恒为 null;区分「hook 没装上」与
* 「装上了但没抓到」。**与规则捕获无关**——它以前就叫 `capture`,排查 #165 时
* 我看见 `"capture": null` 据此写下「没有任何 captureFailure」,结论是错的。
* 改名会让旧排障文件字段对不上,但一个正在制造错误结论的名字代价更大(#166)。
*/
aopengCapture: aopengCaptureStatus(document),
logs: logs.value
};
downloadText(
`aiask-diagnostics-${Date.now()}.json`,
JSON.stringify(payload, null, 2)
);
};
const exportPage = () => downloadText(
`aiask-page-${Date.now()}.html`,
buildPageExportHtml(list.value, {
platformLabel: platformLabel.value,
exportedAt: (/* @__PURE__ */ new Date()).toLocaleString("zh-CN")
}),
"text/html"
);
const exportHarvest = () => downloadText(
`aiask-harvest-${Date.now()}.html`,
buildHarvestExportHtml(harvestedList.value, {
platformLabel: platformLabel.value,
exportedAt: (/* @__PURE__ */ new Date()).toLocaleString("zh-CN")
}),
"text/html"
);
const openLogs = () => {
tab.value = "system";
systemSub.value = "diag";
};
const list = vue.ref([]);
const curInx = vue.ref(0);
const running = vue.ref(false);
const roundStarted = vue.ref(false);
const tip = vue.ref("空闲");
const note2 = vue.ref("");
const noteAction = vue.ref("");
const diag = vue.ref(null);
const lastCaptureFailure = vue.ref(null);
const diagOpen = vue.ref(true);
const ruleDiag = vue.ref(null);
const ruleVersionLabel = vue.computed(() => {
if (ruleDiag.value) return `规则 ${ruleDiag.value.version}`;
return ruleStoreVersions.value.length > 0 ? `规则 ${ruleStoreVersions.value.length} 包 · 本页未匹配` : "规则 未同步";
});
const ruleStoreVersions = vue.ref([]);
const refreshRuleStoreVersions = () => {
ruleStoreVersions.value = ruleStoreRuntime.usablePackageIds();
};
const ruleMetaOpen = vue.ref(false);
const ruleUpdating = vue.ref(false);
const ruleUpdateNote = vue.ref("");
const navOpen = vue.ref(true);
const optsExpanded = vue.ref(false);
vue.watch(curInx, () => {
optsExpanded.value = false;
});
let session = null;
let ctx = null;
let adapter = null;
let createAdapter = null;
const platform = vue.ref(
((_a2 = trustedRemoteRulePlatformFor(location.hostname)) == null ? void 0 : _a2.platform) ?? "chaoxing"
);
const courseAdapter = courseAdapterFor(platform.value);
let loaded = false;
let harvestSettledAt = 0;
let harvestSignature = "";
let detecting = false;
let detectAgain = false;
let autoResumeStarted = false;
let stopFrameReady = null;
let stopRuleStoreUpdates = null;
let stopPageChanges = null;
let stopDomChanges = null;
let stopUrlChanges = null;
let pageChangeScheduler = null;
function currentReportIdentity() {
const rule = ruleDiag.value;
if (!rule) return null;
return buildReportIdentity(
platform.value,
getClientId(),
SCRIPT_VERSION,
RULE_ENGINE_VERSION,
rule
);
}
function refreshRuleDiagnostic() {
var _a3;
const loadStatus = (_a3 = ruleDiag.value) == null ? void 0 : _a3.loadStatus;
if (!adapter || !loadStatus) return;
ruleDiag.value = buildRuleSessionDiagnostic(
adapter,
loadStatus,
ruleStoreRuntime.snapshot().releaseSummaries
);
syncCourseConfig();
}
let courseConfigSource = null;
function syncCourseConfig() {
var _a3, _b, _c;
const remote = (_b = (_a3 = resolvedRulePackage(adapter)) == null ? void 0 : _a3.shellConfig) == null ? void 0 : _b.selectors;
const source = remote ? ((_c = ruleDiag.value) == null ? void 0 : _c.version) ?? "remote" : "built-in";
if (source === courseConfigSource) return;
courseConfigSource = source;
applyCourseConfig(remote);
pushLog(
remote ? `课程判据来自规则包 ${source} · ${Object.keys(remote).length} 项` : (
// 判据其实是 `adapter` 匹没匹上,而非包下没下发:本页不是章测页时必然为空。
// 2026-08-20 真机上这句「规则包未下发」把我引去查同步链路,包其实就在本地。
// 分档说:一个可用包都没有才是真没下发,否则只是这一页没有对应规则。
ruleStoreRuntime.usablePackageIds().length === 0 ? "课程判据用内置默认值 · 本地没有可用规则包" : "课程判据用内置默认值 · 本页无对应规则"
),
"info"
);
}
function examSessionStorage() {
var _a3;
try {
return ((_a3 = document.defaultView) == null ? void 0 : _a3.sessionStorage) ?? null;
} catch {
return null;
}
}
function clearExamAutoResume() {
const storage = examSessionStorage();
if (storage) clearChaoxingExamAutoResume(storage);
}
const stats = vue.computed(() => {
let charged = 0;
for (const it of list.value) if (it.charged) charged++;
return { charged };
});
const PAID_BLOCKED_STATUSES = /* @__PURE__ */ new Set([
"insufficient",
"unauthorized",
"rate_limited"
]);
const runSummary = vue.computed(() => ({
filled: list.value.filter((it) => it.filled).length,
charged: stats.value.charged,
// 命中且扣了分,却没能安全写回页面:钱花了、货没到,是唯一一条「付钱没拿到货」的路径。
chargedUnfilled: list.value.filter((it) => it.charged && !it.filled).length,
// #73:免费源/本地缓存命中但没能写入页面:没花钱,但网格标了「没成」,摘要必须说得出这题去哪了
hitUnfilled: list.value.filter(
(it) => it.status === "hit" && !it.filled && !it.charged
).length,
// 真的查过但没拿到可用答案。**对用户就是「没扣分」**——服务端确实走
// reserve→refunded 的台账,但那是内部结算细节;界面上说「已退款」会让人以为
// 钱先被扣走过,跑去对流水又找不到对应的扣款,反而像出了问题。
missed: list.value.filter(
(it) => (it.status === "miss" || it.status === "unsafe") && !it.answer.length && !(it.answerNode && PAID_BLOCKED_STATUSES.has(it.answerNode.status))
).length,
// 付费查询从未真正发起:余额不足/登录失效/限流——不涉及扣款,无从退起
unqueried: list.value.filter(
(it) => it.answerNode && PAID_BLOCKED_STATUSES.has(it.answerNode.status)
).length,
// 抓题阶段就无法处理:从未查询、从未扣分
skipped: list.value.filter(
(it) => it.status === "decodeFail" || it.status === "unsupported"
).length
}));
const runDone = vue.computed(
() => roundStarted.value && list.value.length > 0 && !running.value && list.value.every((it) => it.status !== "pending")
);
const detectedCount = vue.computed(() => list.value.length);
const features = vue.computed(() => platformFeatures(platform.value));
const visibleSystemSegs = vue.computed(
() => SYSTEM_SEGS.filter((seg) => !seg.feature || hasFeature(seg.feature))
);
const hasFeature = (name) => features.value.includes(name);
const platformLabel = vue.computed(() => platformLabelFor(platform.value));
const pageStatus = vue.computed(() => {
if (list.value.length > 0) return `检测到 ${list.value.length} 题`;
if (harvestedCount.value > 0) return `本页已收录 ${harvestedCount.value} 题`;
if (tip.value === "空闲") return "待命中";
return tip.value;
});
const homeHint = vue.computed(() => {
if (running.value) return tip.value;
if (!detectedCount.value) {
if (harvestedCount.value > 0)
return "正确答案已存入本地缓存 · 下次遇到同题直接命中,不扣分";
return hasFeature("answer") ? "打开作业、考试或章节测验页即自动识别,没反应可手动重新检测" : "打开已批阅的作业结果页即自动收录正确答案";
}
const done = list.value.filter((it) => it.status !== "pending").length;
if (!done) return "已就绪 · 点「开始答题」自动查题并回填";
const hit = list.value.filter((it) => it.status === "hit" && it.filled).length;
return `本轮 ${hit} 命中 / ${detectedCount.value} 题`;
});
const standbyHint = vue.computed(() => {
const missing = missingRulePackage();
if (missing)
return missing.routed ? "本页应由云端规则接管,但规则包还没下载。点「检查更新」。" : "本页暂未支持 · 已记录。章节测验与作业页可正常答题。";
return hasFeature("answer") ? "当前页未发现题目。翻到作业或测验页即自动识别。" : "当前页未发现题目。打开已批阅的作业结果页即自动收录正确答案。";
});
const cur = vue.computed(() => list.value[curInx.value]);
const currentTypeLabel = vue.computed(() => {
var _a3;
if (!cur.value) return "";
if (((_a3 = cur.value.unit) == null ? void 0 : _a3.queryType) === "short_answer") return "简答";
return QUESTION_TYPE_LABELS[cur.value.q.type];
});
const matchedOptionIndexes = vue.computed(() => {
const current = cur.value;
if (!(current == null ? void 0 : current.unit)) return /* @__PURE__ */ new Set();
return new Set(answeredOptionIndexes(current.unit, current.answerPlan));
});
const isHit = (index) => matchedOptionIndexes.value.has(index);
const optionDisclosure = vue.computed(() => {
const c = cur.value;
return resolveOptionDisclosure(
(c == null ? void 0 : c.q.options) ?? [],
matchedOptionIndexes.value,
optsExpanded.value
);
});
const shownOpts = vue.computed(() => optionDisclosure.value.visible);
const collapsible = vue.computed(() => optionDisclosure.value.collapsible);
const treeStatusLabel = (status) => {
if (status === "complete" || status === "hit") return "完整";
if (status === "partial") return "部分";
if (status === "unsafe") return "已拒答";
return "未命中";
};
const headChip = vue.computed(() => {
if (!loggedIn.value) return "";
if (tab.value === "ask" && loaded) return platformLabel.value;
if (tab.value === "home" && accountName.value) return accountName.value;
return "";
});
function discard() {
var _a3;
const discarded = loaded || list.value.length > 0;
pageChangeScheduler == null ? void 0 : pageChangeScheduler.cancel();
stopPageChanges == null ? void 0 : stopPageChanges();
stopPageChanges = null;
void ((_a3 = adapter == null ? void 0 : adapter.dispose) == null ? void 0 : _a3.call(adapter));
session = null;
ctx = null;
adapter = null;
createAdapter = null;
list.value = [];
curInx.value = 0;
harvestedCount.value = 0;
harvestedList.value = [];
diag.value = null;
lastCaptureFailure.value = null;
ruleDiag.value = null;
loaded = false;
harvestSettledAt = 0;
harvestSignature = "";
answeringTask = "";
answeringTicksSeen = 0;
running.value = false;
roundStarted.value = false;
tip.value = "空闲";
if (discarded && tab.value === "ask") tab.value = "home";
}
async function detectQuestions(allowAutoStart = true) {
if (loaded || running.value) return;
if (detecting) {
detectAgain = true;
return;
}
detecting = true;
try {
do {
detectAgain = false;
if (!build()) return;
if (!session || !ctx) {
tip.value = "空闲";
return;
}
const active2 = session;
const activeCtx = ctx;
try {
const n = await active2.load(activeCtx);
if (session !== active2) return;
refreshRuleDiagnostic();
lastCaptureFailure.value = ruleCaptureFailure(adapter);
syncCacheCount();
const harvest = active2.lastHarvest;
harvestedCount.value = (harvest == null ? void 0 : harvest.persisted) ?? 0;
harvestedList.value = (harvest == null ? void 0 : harvest.items) ?? [];
const signature2 = harvestedList.value.map((item) => item.unitHash).join();
harvestSettledAt = harvestedList.value.length > 0 ? Date.now() : 0;
if (harvest && harvest.persisted > 0 && signature2 !== harvestSignature)
pushLog(
cachePersistFailed.value ? `本页收录 ${harvest.persisted} 题 · 未能落盘,关掉页面会丢` : `本页收录 ${harvest.persisted} 题 · 已存入本地缓存`,
cachePersistFailed.value ? "warning" : "info"
);
harvestSignature = signature2;
if (harvest && harvest.persisted < harvest.harvested)
pushLog(
`${harvest.harvested - harvest.persisted} 题收录写入失败 · 未存入缓存`,
"warning"
);
if (n > 0) {
list.value = active2.list;
loaded = true;
roundStarted.value = false;
tip.value = `${platformLabel.value} · 检测到 ${n} 题`;
tab.value = "ask";
pushLog(`命中${platformLabel.value} · 抓到 ${n} 题`, "info");
const storage = examSessionStorage();
if (!autoResumeStarted && storage && shouldAutoResumeChaoxingExam(location, storage)) {
autoResumeStarted = true;
pushLog("已进入整卷预览 · 自动继续答题", "info");
queueMicrotask(() => void start());
} else if (allowAutoStart && settings.autoStart && !running.value && hasFeature("answer")) {
pushLog(`自动开始答题 · ${n} 题`, "info");
queueMicrotask(() => void start());
}
} else {
const readout = zeroQuestionReadout(
platformLabel.value,
lastCaptureFailure.value
);
pushLog(readout.log, readout.level);
if (harvestedList.value.length) tab.value = "harvest";
}
} catch (error) {
const reason = error instanceof Error ? error.message : String(error ?? "");
pushLog(
`题目检测失败,可手动开始答题${reason ? ` · ${reason.slice(0, 80)}` : ""}`,
"warning"
);
}
} while (detectAgain && !loaded);
} finally {
detecting = false;
}
}
async function onFrameReady() {
if (running.value) return;
if (!loaded) {
await detectQuestions();
return;
}
if (!(session == null ? void 0 : session.isStale())) return;
const previous = session.list;
discard();
await detectQuestions(false);
if (session == null ? void 0 : session.adoptResults(previous)) {
list.value = [...session.list];
roundStarted.value = true;
tip.value = `完成 · ${session.stats().hit} 命中 / ${session.list.length} 题`;
pushLog("页面已重载 · 保留本轮结果", "info");
} else {
pushLog("页面已切换 · 重新识别", "info");
if (settings.autoStart && !running.value && hasFeature("answer") && loaded) {
pushLog(`自动开始答题 · ${list.value.length} 题`, "info");
queueMicrotask(() => void start());
}
}
}
vue.onMounted(() => {
pageChangeScheduler = createPageChangeScheduler(window, () => {
if (running.value) return;
discard();
void detectQuestions();
});
const fontStatus = chaoxingFontTableStatus();
if (fontStatus !== "ok")
pushLog(
fontStatus === "unavailable" ? "字体表未下载 · 带加密字体的题目无法识别 · 请重装脚本以重新下载资源" : "字体表内容校验未通过 · 已安全拒用 · 带加密字体的题目无法识别",
"warning"
);
const capture2 = aopengCaptureStatus(document);
if (capture2 && !capture2.installed)
pushLog("接口旁听未装上 · 本页只能靠页面 DOM 收录", "warning");
stopFrameReady = subscribeFrameReady(document, onFrameReady);
stopDomChanges = subscribeDomChanges(document, () => {
if (loaded || running.value) return;
if (harvestSettledAt && Date.now() - harvestSettledAt < HARVEST_RECHECK_MS)
return;
pageChangeScheduler == null ? void 0 : pageChangeScheduler.notify();
});
stopUrlChanges = subscribeUrlChanges(window, () => {
if (running.value) return;
pageChangeScheduler == null ? void 0 : pageChangeScheduler.notify();
});
stopRuleStoreUpdates = subscribeRuleStoreUpdates((result) => {
const readout = ruleUpdateReadout(
result,
ruleStoreRuntime.usablePackageIds().length
);
if (readout) pushLog(readout.log, readout.level);
refreshRuleStoreVersions();
if (result.status !== "updated") return;
if (running.value) return;
discard();
void detectQuestions();
});
refreshRuleStoreVersions();
void devAutoLogin().finally(() => {
void detectQuestions();
});
syncMediaTask();
window.addEventListener("resize", onWindowResize);
document.addEventListener("keydown", onPanelKeydown);
window.addEventListener("pagehide", onPageHide);
});
vue.onBeforeUnmount(() => {
var _a3;
if (captchaRequest) captchaRequest.cancel();
else if (captchaPending) finishCaptcha(new Error("cancelled"));
stopFrameReady == null ? void 0 : stopFrameReady();
stopFrameReady = null;
stopRuleStoreUpdates == null ? void 0 : stopRuleStoreUpdates();
stopRuleStoreUpdates = null;
stopPageChanges == null ? void 0 : stopPageChanges();
stopPageChanges = null;
stopDomChanges == null ? void 0 : stopDomChanges();
stopDomChanges = null;
stopUrlChanges == null ? void 0 : stopUrlChanges();
stopUrlChanges = null;
pageChangeScheduler == null ? void 0 : pageChangeScheduler.dispose();
pageChangeScheduler = null;
mediaRunner == null ? void 0 : mediaRunner.stop();
mediaRunner = null;
void ((_a3 = adapter == null ? void 0 : adapter.dispose) == null ? void 0 : _a3.call(adapter));
if (dragState.pointerId !== null) endPanelDrag(dragState, dragState.pointerId);
panelResizeObserver == null ? void 0 : panelResizeObserver.disconnect();
panelResizeObserver = null;
window.removeEventListener("resize", onWindowResize);
document.removeEventListener("keydown", onPanelKeydown);
window.removeEventListener("pagehide", onPageHide);
});
async function finishRound() {
const active2 = session;
if (!active2) return;
if (settings.randomFallback) {
let picked = 0;
const skipped = [];
for (let i = 0; i < active2.list.length; i += 1) {
if (session !== active2) return;
const reason = await active2.fillRandomWithReason(i);
if (reason === "ok") picked += 1;
else if (reason !== "already-filled") skipped.push(reason);
}
if (picked > 0) {
list.value = [...active2.list];
pushLog(`随机作答 ${picked} 题 · 仅单选与判断`, "warning");
} else {
if (skipped.length > 0)
pushLog(
`随机作答未触发 · ${skipped.length} 题 · 原因 ${[
...new Set(skipped)
].join("/")}`,
"warning"
);
}
}
if (session !== active2) return;
if (stats.value.charged > 0) authStale.value = false;
const answerableCount = countAnswerable();
const ratio = Math.round(trustedRatio(active2.list, answerableCount) * 100);
const submitDocs = () => readableDocuments(document);
let outcome;
try {
outcome = await autoSubmitRound(submitDocs, {
enabled: settings.autoSubmit,
items: active2.list,
answerableCount,
threshold: settings.autoSubmitThreshold,
// 「交没交成」以卷面状态为准,不以「我点过确认」为准(坑 31)。
// 判据直接调课程学习那侧的 chapterTestDone,两处永远同一份。
// **每次重扫帧**:交卷后超星把答题 iframe 换成判分页,旧引用上永远查不到转态。
isSubmitted: () => readableDocuments(document).some(
(doc) => chapterTestDone(doc) === true
),
// 入口走的哪条路要留痕:直调页面函数(确定性高)还是退回模拟点击。
onEntry: (how, source) => {
pushLog(
how === "call" ? "提交入口 · 直接调用页面函数 btnBlueSubmit()" : "提交入口 · 已点击(走站点自己的点击链,直调算不出 pos)",
how === "call" ? "warning" : "info"
);
if (source) pushLog(`入口函数源码 · ${source}`, "info");
},
// 「点了确认」得说得出点的是谁:弹窗关了却没交时,这一行是唯一能分清
// 「点的是确定」还是「点到取消/关闭」的证据。
// 确认这一步走的是站点自己的提交函数——比点那颗闭包对不上的按钮确定得多。
// 点的是站点确认框那颗 `#popok`——2026-08-20 真机对照:直调提交函数会被
// 服务端以「无效的参数:code-1!」拒收,真人点击当场成功。
onConfirmCall: (name) => pushLog(`确认提交 · 已点 ${name}`, "warning"),
// 直调这一步逐帧探到什么、抛了什么。**失败时这一行是唯一线索**——
// 上一版把异常吞了,日志里只剩「点确认」,排查等于从零开始。
onConfirmProbe: (detail) => pushLog(`提交函数探测 · ${detail}`, "info")
});
} catch (error) {
pushLog(
`自动提交异常 · ${error instanceof Error ? error.message.slice(0, 80) : String(error ?? "")}`,
"error"
);
outcome = "click-failed";
}
submitOutcome.value = outcome;
if (outcome === "submitted")
pushLog(`可信命中 ${ratio}% · 已提交 · 卷面已转为已完成`, "warning");
else if (outcome === "confirm-accepted")
pushLog(`可信命中 ${ratio}% · ${SUBMIT_ACCEPTED_NOTE}`, "warning");
else if (outcome !== "off" && outcome !== "no-items") {
pushLog(
`可信命中 ${ratio}% · 未提交 · ${SUBMIT_SKIP_REASON[outcome]}`,
"info"
);
if (outcome === "unrecognized-questions")
pushLog(
`已识别 ${active2.list.length}/${answerableCount} 题 · 差 ${(answerableCount ?? 0) - active2.list.length} 题没被规则认出来`,
"warning"
);
if (outcome === "confirm-unverified")
for (const item of submitCandidates(readableDocuments(document)))
pushLog(
`确认后仍在 · ${item.text} · ${item.tag}.${item.className} · on=${item.handler || "无"}`,
"warning"
);
if (outcome === "no-entry") {
try {
const docs = submitDocs();
pushLog(
`提交取证 · 扫到 ${docs.length} 帧 · ${docs.map((d) => {
var _a3;
let where = "?";
try {
where = ((_a3 = d.location) == null ? void 0 : _a3.pathname) ?? "?";
} catch {
where = "跨域";
}
return `${where.slice(-20)}:${d.querySelectorAll("a,button,input").length}`;
}).join(" ")}`,
"info"
);
for (const item of submitCandidates(docs))
pushLog(
`提交候选 · ${item.text} · ${item.tag}.${item.className} · on=${item.handler || "无"} · 文案锁${item.textLock ? "过" : "否"} · 入口锁${item.entryLock ? "过" : "否"}`,
"info"
);
} catch (error) {
pushLog(
`提交取证失败 · ${error instanceof Error ? error.message : String(error)}`,
"warning"
);
}
}
}
const reportIdentity = currentReportIdentity();
if (settings.reportHealth && reportIdentity)
sendReport(
aiaskTransport,
BACKEND_BASE_URL,
buildHealthReport(
reportIdentity,
true,
active2.list,
true,
captureFailureReason(ruleCaptureFailure(adapter))
)
);
}
function onEvent(e) {
if (session) list.value = [...session.list];
if (e.kind === "question") curInx.value = e.inx;
else if (e.kind === "progress") tip.value = `查题中 ${e.inx + 1}/${e.total}`;
else if (e.kind === "done") {
clearExamAutoResume();
tip.value = e.total === 0 ? "未识别到题目" : `完成 · ${e.hit} 命中 / ${e.total} 题`;
running.value = false;
syncCacheCount();
pushLog(tip.value, e.total === 0 ? "warning" : "info");
void finishRound();
} else if (e.kind === "paused") {
clearExamAutoResume();
tip.value = "已暂停";
running.value = false;
pushLog("已暂停", "info");
void onFrameReady();
} else if (e.kind === "insufficient") {
note2.value = "余额不足 · 免费题库继续 · 去账户页兑换卡密";
noteAction.value = "account";
pushLog("余额不足 · 付费跳过,免费继续", "warning");
} else if (e.kind === "ratelimited") {
note2.value = "付费侧限流 · 已跳过付费,免费题库继续";
noteAction.value = "";
pushLog("付费限流 · 免费继续", "warning");
} else if (e.kind === "search-failed") {
pushLog(
`第 ${e.inx + 1} 题查询失败 · ${e.reason.slice(0, 90) || "未知错误"}`,
"error"
);
} else if (e.kind === "unauthorized") {
markAuthStale();
note2.value = `${AUTH_STALE_NOTE} · 免费题库仍可用`;
noteAction.value = "login";
pushLog(`${AUTH_STALE_NOTE} · 免费继续`, "warning");
}
}
function missingRulePackage() {
var _a3;
const expected = ((_a3 = trustedRemoteRulePlatformFor(location.hostname)) == null ? void 0 : _a3.packageId) ?? validatedRulePackageIdFor(location);
if (expected)
return ruleStoreRuntime.snapshot().store.resolve(expected) === null ? { packageId: expected, routed: true } : null;
return SUPPORTED_HOST_PATTERN.test(location.hostname) && CHA0XING_ANSWERABLE_PATH.test(location.href) ? { packageId: CHA0XING_UNROUTED_PACKAGE_ID, routed: false } : null;
}
let reportedMissingPackageId = null;
function reportMissingRulePackage(packageId) {
if (!settings.reportHealth || reportedMissingPackageId === packageId) return;
reportedMissingPackageId = packageId;
void sendReport(
aiaskTransport,
BACKEND_BASE_URL,
buildHealthReport(
buildMissingRuleReportIdentity(
platform.value,
getClientId(),
SCRIPT_VERSION,
RULE_ENGINE_VERSION,
packageId
),
false,
[],
false
)
);
}
function build() {
var _a3;
if (session && ctx) return true;
const r = createSession({
transport: gmTransport,
backendTransport: aiaskTransport,
document,
location,
typr: Typr$1,
fontTable: getChaoxingFontTable(),
getToken,
baseUrl: BACKEND_BASE_URL,
settings,
localStore: localAnswerCache,
emit: onEvent
});
if (!r.session) {
const missing = missingRulePackage();
tip.value = !missing ? "当前页面未识别到题目" : missing.routed ? "规则包尚未下载 · 请点「检查更新」" : "本页暂未支持";
if (missing) reportMissingRulePackage(missing.packageId);
return false;
}
session = r.session;
ctx = r.ctx;
adapter = r.adapter;
stopPageChanges = ((_a3 = adapter.subscribePageChanges) == null ? void 0 : _a3.call(adapter, () => {
pageChangeScheduler == null ? void 0 : pageChangeScheduler.notify();
})) ?? null;
createAdapter = r.createAdapter;
ruleDiag.value = r.rule;
platform.value = r.platform;
localAnswerCache.setPlatform(platformLabel.value);
syncCourseConfig();
return true;
}
async function runDiag() {
var _a3, _b;
if (!build() || !ctx || !createAdapter) return;
const diagnosticAdapter = createAdapter();
let reportIdentity = currentReportIdentity();
try {
diag.value = await runDiagnostic(diagnosticAdapter, ctx);
lastCaptureFailure.value = ruleCaptureFailure(diagnosticAdapter);
const loadStatus = (_a3 = ruleDiag.value) == null ? void 0 : _a3.loadStatus;
const diagnosticRule = loadStatus ? buildRuleSessionDiagnostic(
diagnosticAdapter,
loadStatus,
ruleStoreRuntime.snapshot().releaseSummaries
) : null;
if (diagnosticRule) {
reportIdentity = buildReportIdentity(
platform.value,
getClientId(),
SCRIPT_VERSION,
RULE_ENGINE_VERSION,
diagnosticRule
);
}
} finally {
await ((_b = diagnosticAdapter.dispose) == null ? void 0 : _b.call(diagnosticAdapter));
}
diagOpen.value = true;
const failureSuffix = lastCaptureFailure.value ? ` · 规则捕获失败 ${lastCaptureFailure.value}` : "";
const summary = diag.value.matched ? `诊断 · 命中${platformLabel.value} · 抓到 ${diag.value.count} 题 · 收录 ${diag.value.harvestedCount} 题${failureSuffix}` : "诊断 · 未命中当前页";
pushLog(
summary,
diag.value.matched && !lastCaptureFailure.value ? "info" : "warning"
);
if (settings.reportHealth && reportIdentity) {
sendReport(
aiaskTransport,
BACKEND_BASE_URL,
buildDiagnosticReport(reportIdentity, {
matched: diag.value.matched,
count: diag.value.count,
imageCount: diag.value.imageCount,
items: diag.value.items.map((item) => ({
type: item.type,
decodeFailed: item.decodeFailed,
optionCount: item.optionCount,
imageCount: item.imageCount,
unsupportedReason: item.unsupportedReason
}))
})
);
}
}
function resetRuleStorageAndReload() {
resetRuleStorage(gmRuleStorage);
location.reload();
}
async function updateRules() {
var _a3;
if (running.value || ruleUpdating.value) return;
ruleUpdating.value = true;
ruleUpdateNote.value = "检查中…";
const result = await checkRuleUpdates(true);
refreshRuleDiagnostic();
ruleUpdating.value = false;
const usable = ruleStoreRuntime.usablePackageIds().length;
ruleUpdateNote.value = ((_a3 = ruleUpdateReadout(result, usable)) == null ? void 0 : _a3.note) ?? "";
}
async function start() {
if (running.value) return;
if (!hasFeature("answer")) {
pushLog("本平台仅收录答案 · 不支持自动答题", "info");
return;
}
note2.value = "";
noteAction.value = "";
running.value = true;
tip.value = "查题中…";
tab.value = "ask";
if (!build() || !session || !ctx) {
running.value = false;
pushLog("当前页未识别到题目", "warning");
return;
}
if (!loaded) {
await session.load(ctx);
refreshRuleDiagnostic();
syncCacheCount();
list.value = session.list;
loaded = true;
}
if (!getToken()) {
note2.value = "未登录 · 当前仅查免费题库";
noteAction.value = "login";
pushLog("未登录 · 仅免费题库", "info");
}
pushLog("开始答题", "info");
roundStarted.value = true;
submitOutcome.value = null;
session.setOptions({
autoFill: true,
delayMs: settings.delayMs,
freeFirst: settings.freeFirst
});
await session.start(curInx.value);
}
async function reAnswerCurrent() {
if (running.value || !session || !cur.value) return;
if (cur.value.status === "unsupported") {
tip.value = "题目内容解析失败,已跳过";
pushLog(tip.value, "warning");
return;
}
running.value = true;
tip.value = `重答第 ${curInx.value + 1} 题…`;
session.setOptions({
autoFill: true,
delayMs: settings.delayMs,
freeFirst: settings.freeFirst
});
pushLog(`重答第 ${curInx.value + 1} 题`, "info");
try {
await session.reAnswer(curInx.value);
list.value = [...session.list];
const item = list.value[curInx.value];
tip.value = (item == null ? void 0 : item.status) === "hit" && item.filled ? "本题重答完成" : (item == null ? void 0 : item.status) === "hit" ? "本题有答案但未回填" : "本题暂未命中";
pushLog(
tip.value,
(item == null ? void 0 : item.status) === "hit" && item.filled ? "info" : "warning"
);
} finally {
running.value = false;
}
}
const pause = () => session == null ? void 0 : session.pause();
const restart = () => {
discard();
start();
};
function jump(i) {
var _a3;
curInx.value = i;
const el = (_a3 = list.value[i]) == null ? void 0 : _a3.q.el;
if (el) {
el.scrollIntoView({ block: "center" });
el.style.outline = "2px solid var(--acc)";
setTimeout(() => {
el.style.outline = "";
}, 600);
}
}
const cellClass = (it, i) => {
if (i === curInx.value) return "cur";
if (it.status === "hit" && it.filled) return "hit";
if (it.status === "miss" || it.status === "unsafe" || it.status === "hit" && !it.filled)
return "miss";
return "";
};
const letter2 = (i) => String.fromCharCode(65 + i);
vue.watch(
() => systemSub.value,
(value) => {
if (value === "cache") refreshCache();
}
);
vue.watch(visibleSystemSegs, (segs) => {
var _a3;
if (!segs.some((seg) => seg.k === systemSub.value))
systemSub.value = ((_a3 = segs[0]) == null ? void 0 : _a3.k) ?? "general";
});
return (_ctx, _cache) => {
var _a3, _b;
return collapsed.value ? (vue.openBlock(), vue.createElementBlock("div", {
key: 0,
ref_key: "panelRef",
ref: panelRef,
class: "bubble",
style: vue.normalizeStyle(panelStyle.value),
onPointerdown: startBubbleDrag,
onPointermove: moveDrag,
onPointerup: finishDrag,
onPointercancel: finishDrag,
onLostpointercapture: finishDrag
}, [
tip.value !== "空闲" ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_1, vue.toDisplayString(tip.value), 1)) : vue.createCommentVNode("", true),
vue.createElementVNode("button", {
class: "launcher",
onClick: activateLauncher,
"aria-label": "展开爱问答"
}, [
_cache[28] || (_cache[28] = vue.createElementVNode("span", { class: "seal s44" }, "问", -1)),
detectedCount.value ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_2, vue.toDisplayString(detectedCount.value), 1)) : vue.createCommentVNode("", true)
])
], 36)) : (vue.openBlock(), vue.createElementBlock("div", {
key: 1,
ref_key: "panelRef",
ref: panelRef,
class: "panel",
style: vue.normalizeStyle(panelStyle.value)
}, [
(vue.openBlock(), vue.createElementBlock("svg", _hoisted_3, [..._cache[29] || (_cache[29] = [
vue.createStaticVNode('', 3)
])])),
vue.createElementVNode("div", {
ref_key: "dragHandleRef",
ref: dragHandleRef,
class: "head",
onPointerdown: startDrag,
onPointermove: moveDrag,
onPointerup: finishDrag,
onPointercancel: finishDrag,
onLostpointercapture: finishDrag
}, [
_cache[32] || (_cache[32] = vue.createElementVNode("span", { class: "seal s22" }, "问", -1)),
_cache[33] || (_cache[33] = vue.createElementVNode("span", { class: "name" }, "爱问答", -1)),
_cache[34] || (_cache[34] = vue.createElementVNode("span", { class: "spacer" }, null, -1)),
headChip.value ? (vue.openBlock(), vue.createElementBlock("span", {
key: 0,
class: vue.normalizeClass(["chip", { mono: tab.value === "home" }])
}, vue.toDisplayString(headChip.value), 3)) : vue.createCommentVNode("", true),
vue.createElementVNode("button", {
class: vue.normalizeClass(["ava", { out: !loggedIn.value }]),
"aria-label": loggedIn.value ? "账户" : "登录",
onClick: vue.withModifiers(toggleAccount, ["stop"])
}, [
loggedIn.value ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
vue.createTextVNode(vue.toDisplayString(avatarInitial.value), 1)
], 64)) : (vue.openBlock(), vue.createElementBlock("svg", _hoisted_5, [..._cache[30] || (_cache[30] = [
vue.createElementVNode("circle", {
cx: "12",
cy: "8",
r: "3.4"
}, null, -1),
vue.createElementVNode("path", {
d: "M5.5 20c1.3-3.6 4-5.4 6.5-5.4s5.2 1.8 6.5 5.4",
"stroke-linecap": "round"
}, null, -1)
])]))
], 10, _hoisted_4),
vue.createElementVNode("button", {
class: "x",
onClick: collapse,
"aria-label": "收起"
}, [..._cache[31] || (_cache[31] = [
vue.createElementVNode("svg", { class: "ic" }, [
vue.createElementVNode("use", { href: "#i-minus" })
], -1)
])])
], 544),
vue.createElementVNode("div", _hoisted_6, [
(vue.openBlock(), vue.createElementBlock(vue.Fragment, null, vue.renderList(TABS, (t) => {
return vue.createElementVNode("button", {
key: t.k,
class: vue.normalizeClass(["tab", { active: tab.value === t.k }]),
onClick: ($event) => tab.value = t.k
}, vue.toDisplayString(t.l), 11, _hoisted_7);
}), 64))
]),
tab.value === "system" ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_8, [
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(visibleSystemSegs.value, (s) => {
return vue.openBlock(), vue.createElementBlock("button", {
key: s.k,
class: vue.normalizeClass(["seg", { active: systemSub.value === s.k }]),
onClick: ($event) => systemSub.value = s.k
}, vue.toDisplayString(s.l), 11, _hoisted_9);
}), 128))
])) : vue.createCommentVNode("", true),
vue.createElementVNode("div", _hoisted_10, [
tab.value === "home" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
hasFeature("course-automation") ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_11, [
vue.createElementVNode("div", _hoisted_12, [
_cache[35] || (_cache[35] = vue.createElementVNode("span", { class: "locator" }, "课程学习", -1)),
vue.createElementVNode("div", _hoisted_13, [
vue.createElementVNode("button", {
class: "btn ghost sm",
onClick: openCourseSettings,
"aria-label": "课程学习设置"
}, "设置"),
vue.unref(onCourseStudyPage) ? (vue.openBlock(), vue.createElementBlock("button", {
key: 0,
class: "btn ghost sm",
onClick: _cache[0] || (_cache[0] = //@ts-ignore
(...args) => vue.unref(toggleCourseAuto) && vue.unref(toggleCourseAuto)(...args))
}, vue.toDisplayString(settings.courseAuto ? "暂停" : "继续"), 1)) : vue.createCommentVNode("", true)
])
]),
vue.unref(legacyCourseUrl) ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
_cache[36] || (_cache[36] = vue.createElementVNode("div", { class: "gate-h course-status" }, "旧版课程页面 · 课程学习只支持新版", -1)),
_cache[37] || (_cache[37] = vue.createElementVNode("div", { class: "cap-mute" }, "超星同一章节有新旧两种页面,切换后账号与进度不变。", -1)),
vue.createElementVNode("button", {
class: "btn ghost sm",
onClick: switchToNewCoursePage
}, "切换新版")
], 64)) : !vue.unref(onCourseStudyPage) ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [
_cache[38] || (_cache[38] = vue.createElementVNode("div", { class: "gate-h course-status" }, "课程学习只在课程章节页运行", -1)),
_cache[39] || (_cache[39] = vue.createElementVNode("div", { class: "cap-mute" }, "打开某门课的章节学习页后,这里会显示进度与状态。", -1))
], 64)) : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 2 }, [
vue.createElementVNode("div", {
class: "gate-h course-status",
style: { "overflow": "hidden", "text-overflow": "ellipsis", "white-space": "nowrap" },
title: courseStatusText.value
}, vue.toDisplayString(courseStatusText.value), 9, _hoisted_14),
coursePositionText.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_15, [
vue.createElementVNode("span", _hoisted_16, vue.toDisplayString(coursePositionText.value), 1),
vue.createElementVNode("button", {
class: "btn ghost sm",
onClick: cyclePlaybackRate
}, vue.toDisplayString(settings.coursePlaybackRate) + "×", 1)
])) : vue.createCommentVNode("", true),
courseCountText.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_17, vue.toDisplayString(courseCountText.value), 1)) : vue.createCommentVNode("", true),
courseSkipped.value.length ? (vue.openBlock(), vue.createElementBlock("details", _hoisted_18, [
vue.createElementVNode("summary", _hoisted_19, "跳过 " + vue.toDisplayString(courseSkipped.value.length) + " 项", 1),
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(courseSkipped.value, (item, i) => {
return vue.openBlock(), vue.createElementBlock("div", {
key: i,
class: "cap-mute"
}, vue.toDisplayString(item.name) + " · " + vue.toDisplayString(skipReasonLabel(item.reason)), 1);
}), 128))
])) : vue.createCommentVNode("", true),
_cache[40] || (_cache[40] = vue.createElementVNode("div", { class: "cap-mute" }, "暂停会同时停下正在播放的视频。", -1))
], 64))
])) : vue.createCommentVNode("", true),
!detectedCount.value && !harvestedCount.value && !settings.courseAuto ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_20, [
_cache[41] || (_cache[41] = vue.createElementVNode("div", { class: "standby-title" }, "静候一问", -1)),
vue.createElementVNode("div", _hoisted_21, vue.toDisplayString(standbyHint.value), 1)
])) : (vue.openBlock(), vue.createElementBlock("div", _hoisted_22, [
vue.createElementVNode("div", _hoisted_23, [
_cache[42] || (_cache[42] = vue.createElementVNode("span", { class: "locator" }, "页面状态", -1)),
vue.createElementVNode("span", _hoisted_24, vue.toDisplayString(platformLabel.value), 1)
]),
vue.createElementVNode("div", _hoisted_25, vue.toDisplayString(pageStatus.value), 1),
vue.createElementVNode("div", _hoisted_26, vue.toDisplayString(homeHint.value), 1)
])),
vue.createElementVNode("div", _hoisted_27, [
vue.createElementVNode("button", {
class: "btn ghost sm",
onClick: openLogs
}, "运行日志"),
detectedCount.value ? (vue.openBlock(), vue.createElementBlock("button", {
key: 0,
class: "btn ghost sm",
onClick: exportPage
}, "导出本页题目")) : vue.createCommentVNode("", true)
]),
vue.createElementVNode("div", _hoisted_28, [
_cache[43] || (_cache[43] = vue.createElementVNode("div", { class: "sep" }, null, -1)),
vue.createElementVNode("div", _hoisted_29, [
vue.createElementVNode("span", _hoisted_30, vue.toDisplayString(!loggedIn.value ? "未登录" : authStale.value ? `${accountName.value || "账号"} · 需重新验证` : accountName.value || "已登录"), 1),
vue.createElementVNode("div", _hoisted_31, [
loggedIn.value && balance.value != null ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_32, "余额 " + vue.toDisplayString(balance.value) + " 分", 1)) : vue.createCommentVNode("", true),
vue.createElementVNode("button", {
class: "btn ghost sm",
onClick: _cache[1] || (_cache[1] = ($event) => accountOpen.value = true)
}, vue.toDisplayString(loggedIn.value ? "账户" : "登录"), 1)
])
]),
vue.createElementVNode("div", _hoisted_33, vue.toDisplayString(loggedIn.value ? "付费题库找到可用答案后扣分;免费答案不扣分,命中本机收录也不扣分。" : "未登录时仅查询免费题库。"), 1)
])
], 64)) : tab.value === "ask" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [
note2.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_34, [
vue.createElementVNode("span", _hoisted_35, vue.toDisplayString(note2.value), 1),
noteAction.value === "account" ? (vue.openBlock(), vue.createElementBlock("button", {
key: 0,
class: "btn ghost sm sub",
onClick: _cache[2] || (_cache[2] = ($event) => accountOpen.value = true)
}, [..._cache[44] || (_cache[44] = [
vue.createTextVNode("去账户 ", -1),
vue.createElementVNode("svg", { class: "ic sm" }, [
vue.createElementVNode("use", { href: "#i-arrow" })
], -1)
])])) : noteAction.value === "login" ? (vue.openBlock(), vue.createElementBlock("button", {
key: 1,
class: "btn ghost sm sub",
onClick: _cache[3] || (_cache[3] = ($event) => accountOpen.value = true)
}, [..._cache[45] || (_cache[45] = [
vue.createTextVNode("去登录 ", -1),
vue.createElementVNode("svg", { class: "ic sm" }, [
vue.createElementVNode("use", { href: "#i-arrow" })
], -1)
])])) : vue.createCommentVNode("", true)
])) : vue.createCommentVNode("", true),
runDone.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_36, [
_cache[59] || (_cache[59] = vue.createElementVNode("div", { class: "ctitle" }, "本轮完成", -1)),
vue.createElementVNode("div", _hoisted_37, [
_cache[46] || (_cache[46] = vue.createElementVNode("span", { class: "k" }, "已回填", -1)),
vue.createElementVNode("b", null, vue.toDisplayString(runSummary.value.filled) + " 题", 1),
_cache[47] || (_cache[47] = vue.createElementVNode("span", { class: "cap-mute" }, "已暂存", -1))
]),
vue.createElementVNode("div", _hoisted_38, [
_cache[48] || (_cache[48] = vue.createElementVNode("span", { class: "k" }, "实扣", -1)),
vue.createElementVNode("b", null, vue.toDisplayString(runSummary.value.charged) + " 分", 1)
]),
vue.createElementVNode("div", _hoisted_39, [
_cache[49] || (_cache[49] = vue.createElementVNode("span", { class: "k" }, "未命中", -1)),
vue.createElementVNode("b", null, vue.toDisplayString(runSummary.value.missed) + " 题", 1),
_cache[50] || (_cache[50] = vue.createElementVNode("span", { class: "cap-mute" }, "未扣分", -1))
]),
runSummary.value.unqueried ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_40, [
_cache[51] || (_cache[51] = vue.createElementVNode("span", { class: "k" }, "未查询", -1)),
vue.createElementVNode("b", null, vue.toDisplayString(runSummary.value.unqueried) + " 题", 1),
_cache[52] || (_cache[52] = vue.createElementVNode("span", { class: "cap-mute" }, "未发起付费查询 · 未扣分", -1))
])) : vue.createCommentVNode("", true),
runSummary.value.chargedUnfilled ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_41, [
_cache[53] || (_cache[53] = vue.createElementVNode("span", { class: "k" }, "已扣未填", -1)),
vue.createElementVNode("b", null, vue.toDisplayString(runSummary.value.chargedUnfilled) + " 题", 1),
_cache[54] || (_cache[54] = vue.createElementVNode("span", { class: "cap-mute" }, "未能安全写入页面 · 已扣分,需手动核对", -1))
])) : vue.createCommentVNode("", true),
runSummary.value.hitUnfilled ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_42, [
_cache[55] || (_cache[55] = vue.createElementVNode("span", { class: "k" }, "有答案未写入", -1)),
vue.createElementVNode("b", null, vue.toDisplayString(runSummary.value.hitUnfilled) + " 题", 1),
_cache[56] || (_cache[56] = vue.createElementVNode("span", { class: "cap-mute" }, "未能安全写入页面 · 未扣分,可展开核对", -1))
])) : vue.createCommentVNode("", true),
runSummary.value.skipped ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_43, [
_cache[57] || (_cache[57] = vue.createElementVNode("span", { class: "k" }, "未处理", -1)),
vue.createElementVNode("b", null, vue.toDisplayString(runSummary.value.skipped) + " 题", 1),
_cache[58] || (_cache[58] = vue.createElementVNode("span", { class: "cap-mute" }, "解析失败或题型不支持 · 未查询、未扣分", -1))
])) : vue.createCommentVNode("", true),
vue.createElementVNode("div", _hoisted_44, vue.toDisplayString(submitNote.value), 1),
_cache[60] || (_cache[60] = vue.createElementVNode("span", {
class: "done-seal",
"aria-hidden": "true"
}, "答", -1))
])) : vue.createCommentVNode("", true),
vue.createElementVNode("div", _hoisted_45, [
!list.value.length ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_46, vue.toDisplayString(tip.value === "空闲" ? "当前页未识别到题目 · 打开作业页后自动切入" : tip.value), 1)) : vue.createCommentVNode("", true),
_cache[62] || (_cache[62] = vue.createElementVNode("div", { class: "cap-mute" }, "付费题库找到可用答案后扣分;免费答案不扣分;无法安全匹配时不会回填。", -1)),
stats.value.charged ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_47, [
_cache[61] || (_cache[61] = vue.createElementVNode("span", { class: "spacer" }, null, -1)),
vue.createElementVNode("span", _hoisted_48, "付费题库命中 " + vue.toDisplayString(stats.value.charged) + " 题", 1)
])) : vue.createCommentVNode("", true),
stats.value.charged ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_49, "重复答题会复用已扣分结果,不会重复扣分。")) : vue.createCommentVNode("", true)
]),
list.value.length ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_50, [
vue.createElementVNode("button", {
class: "fold",
onClick: _cache[4] || (_cache[4] = ($event) => navOpen.value = !navOpen.value)
}, [
_cache[64] || (_cache[64] = vue.createTextVNode("题目导航", -1)),
(vue.openBlock(), vue.createElementBlock("svg", {
class: vue.normalizeClass(["ic sm chev", { right: !navOpen.value }])
}, [..._cache[63] || (_cache[63] = [
vue.createElementVNode("use", { href: "#i-chevron" }, null, -1)
])], 2))
]),
navOpen.value ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
_cache[65] || (_cache[65] = vue.createStaticVNode('当前已答未答无答案
', 1)),
vue.createElementVNode("div", _hoisted_51, [
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(list.value, (it, i) => {
return vue.openBlock(), vue.createElementBlock("button", {
key: i,
class: vue.normalizeClass(["cell", cellClass(it, i)]),
onClick: ($event) => jump(i)
}, vue.toDisplayString(i + 1), 11, _hoisted_52);
}), 128))
])
], 64)) : vue.createCommentVNode("", true)
])) : vue.createCommentVNode("", true),
cur.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_53, [
cur.value.status === "decodeFail" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
vue.createElementVNode("div", _hoisted_54, [
vue.createElementVNode("span", _hoisted_55, "第 " + vue.toDisplayString(curInx.value + 1) + " 题", 1),
_cache[66] || (_cache[66] = vue.createElementVNode("span", { class: "tag neutral" }, "解码失败", -1))
]),
_cache[67] || (_cache[67] = vue.createElementVNode("div", { class: "stem" }, "(题面解析失败,已跳过)", -1)),
_cache[68] || (_cache[68] = vue.createElementVNode("div", { class: "cap-mute" }, "解析失败 · 未扣分 · 需手动核对", -1))
], 64)) : cur.value.status === "unsupported" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [
vue.createElementVNode("div", _hoisted_56, [
vue.createElementVNode("span", _hoisted_57, "第 " + vue.toDisplayString(curInx.value + 1) + " 题", 1),
_cache[69] || (_cache[69] = vue.createElementVNode("span", { class: "tag neutral" }, "内容解析失败", -1))
]),
_cache[70] || (_cache[70] = vue.createElementVNode("div", { class: "stem" }, "(题目无合法文字或图片,已跳过)", -1)),
_cache[71] || (_cache[71] = vue.createElementVNode("div", { class: "cap-mute" }, "未搜索 · 未扣分 · 未回填", -1))
], 64)) : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 2 }, [
vue.createElementVNode("div", _hoisted_58, [
vue.createElementVNode("span", _hoisted_59, "第 " + vue.toDisplayString(curInx.value + 1) + " 题", 1),
vue.createElementVNode("div", _hoisted_60, [
cur.value.treeProgress && cur.value.treeProgress.total > 1 ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_61, " 父题 " + vue.toDisplayString(cur.value.treeProgress.hit) + "/" + vue.toDisplayString(cur.value.treeProgress.total) + " · " + vue.toDisplayString(treeStatusLabel(cur.value.treeProgress.status)), 1)) : vue.createCommentVNode("", true),
vue.createElementVNode("button", {
class: "btn ghost sm sub",
disabled: running.value,
onClick: reAnswerCurrent
}, "重答本题", 8, _hoisted_62)
])
]),
vue.createElementVNode("div", _hoisted_63, [
vue.createElementVNode("span", _hoisted_64, "[" + vue.toDisplayString(currentTypeLabel.value) + "]", 1),
vue.createVNode(_sfc_main$1, {
content: cur.value.q.stem,
"max-height": "180px"
}, null, 8, ["content"])
]),
vue.createElementVNode("div", _hoisted_65, [
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(shownOpts.value, (x) => {
return vue.openBlock(), vue.createElementBlock("div", {
key: x.i,
class: "optrow"
}, [
vue.createElementVNode("span", {
class: vue.normalizeClass(["opt", { hit: isHit(x.i) }])
}, [
vue.createTextVNode(vue.toDisplayString(letter2(x.i)) + ". ", 1),
vue.createVNode(_sfc_main$1, {
content: x.o,
"max-height": "120px"
}, null, 8, ["content"])
], 2)
]);
}), 128))
]),
collapsible.value ? (vue.openBlock(), vue.createElementBlock("button", {
key: 0,
class: "expand",
style: { "align-self": "flex-start" },
onClick: _cache[5] || (_cache[5] = ($event) => optsExpanded.value = !optsExpanded.value)
}, [
vue.createTextVNode(vue.toDisplayString(optsExpanded.value ? "收起选项" : `展开选项(${cur.value.q.options.length})`) + " ", 1),
(vue.openBlock(), vue.createElementBlock("svg", {
class: vue.normalizeClass(["ic sm chev", { right: !optsExpanded.value }])
}, [..._cache[72] || (_cache[72] = [
vue.createElementVNode("use", { href: "#i-chevron" }, null, -1)
])], 2))
])) : vue.createCommentVNode("", true),
vue.createElementVNode("div", _hoisted_66, [
vue.createElementVNode("div", _hoisted_67, [
_cache[73] || (_cache[73] = vue.createElementVNode("span", { class: "answer-label" }, "参考答案", -1)),
vue.createElementVNode("div", _hoisted_68, [
cur.value.aiGenerated ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_69, "AI 生成 · 待核对")) : vue.createCommentVNode("", true),
cur.value.answer.length ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_70, vue.toDisplayString(cur.value.filled ? "已回填" : "匹配失败"), 1)) : vue.createCommentVNode("", true)
])
]),
_cache[75] || (_cache[75] = vue.createElementVNode("div", { class: "cap-mute" }, "答案仅供参考,自行核对。", -1)),
((_a3 = cur.value.answerPlan) == null ? void 0 : _a3.kind) === "slots" ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_71, [
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(cur.value.answerPlan.slots, (slot, slotIndex) => {
return vue.openBlock(), vue.createElementBlock("div", {
key: slot.slotId,
class: "answer-item"
}, [
vue.createElementVNode("span", _hoisted_72, "空 " + vue.toDisplayString(slotIndex + 1), 1),
vue.createElementVNode("span", _hoisted_73, [
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(slot.values, (value, valueIndex) => {
return vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: valueIndex }, [
valueIndex ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_74, "、")) : vue.createCommentVNode("", true),
vue.createVNode(_sfc_main$1, {
content: value,
"max-height": "120px"
}, null, 8, ["content"])
], 64);
}), 128))
])
]);
}), 128))
])) : ((_b = cur.value.answerPlan) == null ? void 0 : _b.kind) === "matching-pair" ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_75, [
_cache[74] || (_cache[74] = vue.createElementVNode("span", { class: "answer-key" }, "配对", -1)),
vue.createElementVNode("span", _hoisted_76, [
vue.createVNode(_sfc_main$1, {
content: cur.value.answerPlan.displayValue,
"max-height": "120px"
}, null, 8, ["content"])
])
])) : cur.value.answer.length ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_77, [
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(cur.value.answer, (answer, index) => {
return vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: index }, [
index ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_78, "、")) : vue.createCommentVNode("", true),
vue.createVNode(_sfc_main$1, {
content: answer,
"max-height": "120px"
}, null, 8, ["content"])
], 64);
}), 128))
])) : (vue.openBlock(), vue.createElementBlock("div", _hoisted_79, vue.toDisplayString(cur.value.status === "pending" ? "等待查题" : "暂未找到答案"), 1))
])
], 64))
])) : vue.createCommentVNode("", true)
], 64)) : tab.value === "harvest" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 2 }, [
harvestedList.value.length ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
vue.createElementVNode("div", _hoisted_80, [
vue.createElementVNode("div", _hoisted_81, [
_cache[76] || (_cache[76] = vue.createElementVNode("span", { class: "locator" }, "本页收录", -1)),
vue.createElementVNode("div", _hoisted_82, [
vue.createElementVNode("span", _hoisted_83, vue.toDisplayString(harvestedList.value.length) + " 题", 1),
vue.createElementVNode("button", {
class: "btn ghost sm",
onClick: exportHarvest
}, "导出本页收录")
])
]),
_cache[77] || (_cache[77] = vue.createElementVNode("div", { class: "cap-mute" }, "做过并出分的题目已收录到本机,命中不扣分、不联网。全部记录与备份在「系统 · 缓存」。", -1))
]),
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(harvestedList.value, (h, i) => {
return vue.openBlock(), vue.createElementBlock("div", {
key: h.unitHash,
class: "ent"
}, [
vue.createElementVNode("div", _hoisted_84, [
vue.createElementVNode("span", _hoisted_85, vue.toDisplayString(h.stem ? vue.unref(harvestTypeLabel)(h.itemType) : "无题面"), 1),
vue.createElementVNode("span", _hoisted_86, vue.toDisplayString(i + 1), 1),
!h.persisted ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_87, "· 未落盘")) : vue.createCommentVNode("", true)
]),
vue.createElementVNode("div", {
class: vue.normalizeClass(["ent-q", { "cap-mute": !h.stem }])
}, vue.toDisplayString(h.stem || "这条没有题面(来源未提供),仍可正常命中"), 3),
vue.createElementVNode("div", _hoisted_88, vue.toDisplayString(h.values.join("、")), 1),
h.options && h.options.length ? (vue.openBlock(), vue.createElementBlock("details", _hoisted_89, [
vue.createElementVNode("summary", _hoisted_90, "选项 " + vue.toDisplayString(h.options.length) + " 项", 1),
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(h.options, (op, oi) => {
return vue.openBlock(), vue.createElementBlock("div", {
key: oi,
class: "cap-mute"
}, vue.toDisplayString(letter2(oi)) + "、" + vue.toDisplayString(op), 1);
}), 128))
])) : vue.createCommentVNode("", true)
]);
}), 128))
], 64)) : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [
vue.createElementVNode("div", _hoisted_91, [
_cache[78] || (_cache[78] = vue.createElementVNode("div", { class: "standby-title" }, "本页暂无收录", -1)),
vue.createElementVNode("div", _hoisted_92, "打开已批阅的作业或考试结果页,会自动把你做对的题收录到本机。累计已收录 " + vue.toDisplayString(localCacheCount.value) + " 题,全部记录在「系统 · 缓存」。", 1)
]),
vue.createElementVNode("button", {
class: "btn ghost block",
onClick: goCacheManage
}, "去缓存管理")
], 64))
], 64)) : tab.value === "system" && systemSub.value === "general" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 3 }, [
hasFeature("answer") ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_93, [
_cache[82] || (_cache[82] = vue.createElementVNode("div", { class: "gh2" }, "答题行为", -1)),
vue.createElementVNode("div", _hoisted_94, [
_cache[79] || (_cache[79] = vue.createElementVNode("span", { class: "lbl" }, "答题间隔", -1)),
vue.createElementVNode("span", _hoisted_95, vue.toDisplayString(settings.delayMs) + " ms", 1)
]),
vue.withDirectives(vue.createElementVNode("input", {
class: "range",
type: "range",
min: "500",
max: "4000",
step: "500",
"onUpdate:modelValue": _cache[6] || (_cache[6] = ($event) => settings.delayMs = $event),
onChange: persist
}, null, 544), [
[
vue.vModelText,
settings.delayMs,
void 0,
{ number: true }
]
]),
_cache[83] || (_cache[83] = vue.createElementVNode("div", { class: "cap-mute" }, "相邻两题之间的处理间隔", -1)),
(vue.openBlock(), vue.createElementBlock(vue.Fragment, null, vue.renderList(GENERAL_SWITCHES, (s) => {
return vue.openBlock(), vue.createElementBlock(vue.Fragment, {
key: s.key
}, [
vue.createElementVNode("div", _hoisted_96, [
vue.createElementVNode("button", {
class: vue.normalizeClass(["switch", { off: !settings[s.key] }]),
onClick: s.toggle,
"aria-label": `${s.label}开关`
}, [..._cache[80] || (_cache[80] = [
vue.createElementVNode("i", null, null, -1)
])], 10, _hoisted_97),
vue.createElementVNode("span", _hoisted_98, vue.toDisplayString(s.label), 1)
]),
vue.createElementVNode("div", _hoisted_99, vue.toDisplayString(s.hint), 1)
], 64);
}), 64)),
vue.createElementVNode("div", _hoisted_100, [
_cache[81] || (_cache[81] = vue.createElementVNode("span", { class: "lbl" }, "提交阈值", -1)),
vue.createElementVNode("span", _hoisted_101, "可信命中 ≥ " + vue.toDisplayString(Math.round(settings.autoSubmitThreshold * 100)) + "%", 1)
]),
vue.withDirectives(vue.createElementVNode("input", {
class: "range",
type: "range",
min: "0.5",
max: "1",
step: "0.05",
"onUpdate:modelValue": _cache[7] || (_cache[7] = ($event) => settings.autoSubmitThreshold = $event),
onChange: persist
}, null, 544), [
[
vue.vModelText,
settings.autoSubmitThreshold,
void 0,
{ number: true }
]
]),
_cache[84] || (_cache[84] = vue.createElementVNode("div", { class: "cap-mute" }, "达到阈值才提交,低于只暂存。随机作答填的空不算可信命中。", -1))
])) : vue.createCommentVNode("", true),
_cache[92] || (_cache[92] = vue.createElementVNode("div", { class: "sep" }, null, -1)),
vue.createElementVNode("div", _hoisted_102, [
_cache[87] || (_cache[87] = vue.createElementVNode("div", { class: "gh2" }, "隐私", -1)),
vue.createElementVNode("div", _hoisted_103, [
vue.createElementVNode("button", {
class: vue.normalizeClass(["switch", { off: !settings.reportHealth }]),
onClick: _cache[8] || (_cache[8] = //@ts-ignore
(...args) => vue.unref(toggleReport) && vue.unref(toggleReport)(...args)),
"aria-label": "上报匿名健康开关"
}, [..._cache[85] || (_cache[85] = [
vue.createElementVNode("i", null, null, -1)
])], 2),
_cache[86] || (_cache[86] = vue.createElementVNode("span", {
class: "lbl",
style: { "flex": "1" }
}, "上报匿名健康", -1))
]),
_cache[88] || (_cache[88] = vue.createElementVNode("div", { class: "cap-mute" }, "仅上报命中率与题型,不含题面与账号", -1))
]),
_cache[93] || (_cache[93] = vue.createElementVNode("div", { class: "sep" }, null, -1)),
vue.createElementVNode("div", _hoisted_104, [
_cache[91] || (_cache[91] = vue.createElementVNode("div", { class: "gh2" }, "数据与更新", -1)),
vue.createElementVNode("div", _hoisted_105, [
vue.createElementVNode("div", null, [
_cache[89] || (_cache[89] = vue.createElementVNode("div", { class: "lbl" }, "本地答案缓存", -1)),
vue.createElementVNode("div", _hoisted_106, "已收录 " + vue.toDisplayString(localCacheCount.value) + " 题 · 只存你做过并出分的题目 · 命中不扣分、不联网", 1)
]),
vue.createElementVNode("button", {
class: "btn ghost sm",
onClick: _cache[9] || (_cache[9] = ($event) => systemSub.value = "cache")
}, "管理")
]),
cachePersistFailed.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_107, "存不下了 · 本机存储写入被拒,最近的收录没有落盘。到缓存页导出备份并清理。")) : cacheOverWarn.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_108, "已超出建议容量 " + vue.toDisplayString(vue.unref(CACHE_WARN_ENTRIES)) + " 题 · 不会自动删除记录,建议导出备份后清理。", 1)) : vue.createCommentVNode("", true),
vue.createElementVNode("div", _hoisted_109, [
_cache[90] || (_cache[90] = vue.createElementVNode("div", null, [
vue.createElementVNode("div", { class: "lbl" }, "规则更新"),
vue.createElementVNode("div", { class: "cap-mute" }, "每 24 小时自动检查 · 每个规则包独立验签")
], -1)),
vue.createElementVNode("button", {
class: "btn ghost sm",
disabled: running.value || ruleUpdating.value,
onClick: updateRules
}, vue.toDisplayString(ruleUpdating.value ? "检查中…" : "检查更新"), 9, _hoisted_110)
]),
ruleUpdateNote.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_111, vue.toDisplayString(ruleUpdateNote.value), 1)) : vue.createCommentVNode("", true)
])
], 64)) : tab.value === "system" && systemSub.value === "course" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 4 }, [
vue.createElementVNode("div", _hoisted_112, [
_cache[98] || (_cache[98] = vue.createElementVNode("div", { class: "gh2" }, "学习行为", -1)),
vue.createElementVNode("div", _hoisted_113, [
vue.createElementVNode("button", {
class: vue.normalizeClass(["switch", { off: !settings.courseAuto }]),
onClick: _cache[10] || (_cache[10] = //@ts-ignore
(...args) => vue.unref(toggleCourseAuto) && vue.unref(toggleCourseAuto)(...args)),
"aria-label": "任务点自动播放开关"
}, [..._cache[94] || (_cache[94] = [
vue.createElementVNode("i", null, null, -1)
])], 2),
_cache[95] || (_cache[95] = vue.createElementVNode("span", {
class: "lbl",
style: { "flex": "1" }
}, "自动播放视频/音频(实验)", -1))
]),
vue.createElementVNode("div", _hoisted_114, [
_cache[96] || (_cache[96] = vue.createElementVNode("span", { class: "lbl" }, "播放倍速", -1)),
vue.createElementVNode("span", _hoisted_115, vue.toDisplayString(settings.coursePlaybackRate) + "×", 1)
]),
vue.withDirectives(vue.createElementVNode("input", {
class: "range",
type: "range",
min: "1",
max: "2",
step: "0.5",
"onUpdate:modelValue": _cache[11] || (_cache[11] = ($event) => settings.coursePlaybackRate = $event),
onChange: persist
}, null, 544), [
[
vue.vModelText,
settings.coursePlaybackRate,
void 0,
{ number: true }
]
]),
vue.createElementVNode("div", _hoisted_116, [
_cache[97] || (_cache[97] = vue.createElementVNode("span", { class: "lbl" }, "当前状态", -1)),
vue.createElementVNode("span", _hoisted_117, vue.toDisplayString(mediaStatusText.value), 1)
])
]),
_cache[102] || (_cache[102] = vue.createElementVNode("div", { class: "sep" }, null, -1)),
vue.createElementVNode("div", _hoisted_118, [
_cache[100] || (_cache[100] = vue.createElementVNode("div", { class: "gh2" }, "处理哪些任务点", -1)),
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(taskToggles), (k) => {
return vue.openBlock(), vue.createElementBlock("div", {
class: "switch-row",
key: k
}, [
vue.createElementVNode("button", {
class: vue.normalizeClass(["switch", { off: !settings.courseTaskToggles[k] }]),
onClick: ($event) => toggleTaskKind(k),
"aria-label": `${vue.unref(taskToggleLabel)[k]}任务点开关`
}, [..._cache[99] || (_cache[99] = [
vue.createElementVNode("i", null, null, -1)
])], 10, _hoisted_119),
vue.createElementVNode("span", _hoisted_120, vue.toDisplayString(vue.unref(taskToggleLabel)[k]), 1)
]);
}), 128)),
_cache[101] || (_cache[101] = vue.createElementVNode("div", { class: "cap-mute" }, "关掉的类型直接跳过,也不计入本节还剩多少没做。", -1))
])
], 64)) : tab.value === "system" && systemSub.value === "cache" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 5 }, [
cacheImportPreview.value ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
_cache[108] || (_cache[108] = vue.createElementVNode("div", { class: "ctitle" }, "导入缓存", -1)),
vue.createElementVNode("div", _hoisted_121, [
vue.createElementVNode("div", _hoisted_122, [
_cache[103] || (_cache[103] = vue.createElementVNode("span", { class: "k" }, "文件内", -1)),
vue.createElementVNode("b", null, vue.toDisplayString(cacheImportPreview.value.fileCount) + " 条", 1)
]),
vue.createElementVNode("div", _hoisted_123, [
_cache[104] || (_cache[104] = vue.createElementVNode("span", { class: "k" }, "将新增", -1)),
vue.createElementVNode("b", null, vue.toDisplayString(cacheImportPreview.value.added) + " 条", 1)
]),
vue.createElementVNode("div", _hoisted_124, [
_cache[105] || (_cache[105] = vue.createElementVNode("span", { class: "k" }, "将覆盖", -1)),
vue.createElementVNode("b", null, vue.toDisplayString(cacheImportPreview.value.replaced) + " 条", 1),
_cache[106] || (_cache[106] = vue.createElementVNode("span", { class: "cap-mute" }, "同题将被替换", -1))
]),
vue.createElementVNode("div", _hoisted_125, [
_cache[107] || (_cache[107] = vue.createElementVNode("span", { class: "k" }, "导入后", -1)),
vue.createElementVNode("b", null, vue.toDisplayString(cacheImportPreview.value.total) + " 题", 1)
])
]),
cacheImportPreview.value.replaced ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_126, "将覆盖 " + vue.toDisplayString(cacheImportPreview.value.replaced) + " 条已有记录 · 里面可能有你做过并出分后收录的答案,导入会用文件里的答案顶掉它们,顶掉后不可撤销。想留底就先取消,导出一份再导入。", 1)) : vue.createCommentVNode("", true),
_cache[109] || (_cache[109] = vue.createElementVNode("div", { class: "cap-mute" }, "导入的答案命中时不扣分。爱问答不核验导入内容是否正确,提交作业前自行核对。", -1)),
_cache[110] || (_cache[110] = vue.createElementVNode("div", { class: "cap-mute" }, "导入不会淘汰已有记录,也不会改动已回填的页面或触发提交。", -1))
], 64)) : cacheClearPending.value ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [
_cache[113] || (_cache[113] = vue.createElementVNode("div", { class: "ctitle" }, "清空缓存", -1)),
vue.createElementVNode("div", _hoisted_127, [
vue.createElementVNode("div", _hoisted_128, [
_cache[111] || (_cache[111] = vue.createElementVNode("span", { class: "k" }, "将清空", -1)),
vue.createElementVNode("b", null, vue.toDisplayString(cacheEntries.value.length) + " 题", 1)
]),
_cache[112] || (_cache[112] = vue.createElementVNode("div", { class: "prow" }, [
vue.createElementVNode("span", { class: "k" }, "影响"),
vue.createElementVNode("span", null, "再遇到这些题需重新查询,付费命中会重新扣分。")
], -1))
]),
_cache[114] || (_cache[114] = vue.createElementVNode("div", { class: "cap-mute" }, "清空不可撤销。导出可留一份备份。", -1))
], 64)) : !cacheEntries.value.length ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 2 }, [
_cache[115] || (_cache[115] = vue.createElementVNode("div", { class: "standby" }, [
vue.createElementVNode("div", { class: "standby-title" }, "尚无缓存"),
vue.createElementVNode("div", { class: "cap-mute" }, "做过并出分的题目会被收录到本机,这是缓存的唯一来源;题库答案不入缓存。下次遇到同题直接命中,不扣分、不联网。")
], -1)),
vue.createElementVNode("button", {
class: "btn ghost block",
onClick: pickImportFile
}, "从文件导入"),
vue.createElementVNode("a", {
class: "btn ghost block",
href: PARSE_IMPORT_URL,
target: "_blank",
rel: "noopener noreferrer"
}, "解析导入")
], 64)) : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 3 }, [
vue.createElementVNode("div", _hoisted_129, [
vue.createElementVNode("span", _hoisted_130, [
vue.createElementVNode("b", null, vue.toDisplayString(cacheEntries.value.length), 1),
vue.createTextVNode(" / " + vue.toDisplayString(vue.unref(CACHE_WARN_ENTRIES)) + " 题", 1)
]),
vue.createElementVNode("div", _hoisted_131, [
vue.createElementVNode("i", {
class: vue.normalizeClass({ over: cacheOverWarn.value }),
style: vue.normalizeStyle({ width: `${Math.min(100, cacheEntries.value.length / vue.unref(CACHE_WARN_ENTRIES) * 100)}%` })
}, null, 6)
])
]),
cachePersistFailed.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_132, "存不下了 · 本机存储写入被拒,最近的收录没有落盘。先导出备份,再删掉一些不需要的记录。")) : cacheOverWarn.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_133, "已超出建议容量 · 不会自动删除任何记录,但表越大写入越慢。建议导出备份后清理不再需要的。")) : cacheNearWarn.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_134, "接近建议容量 " + vue.toDisplayString(vue.unref(CACHE_WARN_ENTRIES)) + " 题 · 可先导出备份。", 1)) : vue.createCommentVNode("", true),
_cache[118] || (_cache[118] = vue.createElementVNode("div", { class: "cap-mute" }, "命中缓存不扣分、不联网。只收录你做过并出分的题目,不会自动删除。", -1)),
importedNeverHit.value.total ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 3 }, [
importedNeverHit.value.neverHit === importedNeverHit.value.total ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_135, "导入的 " + vue.toDisplayString(importedNeverHit.value.total) + " 条一条都还没命中过 · 如果其中的题你已经做到过,多半是题面与页面对不上。先拿一道已知的题验一次再说。", 1)) : (vue.openBlock(), vue.createElementBlock("div", _hoisted_136, "导入 " + vue.toDisplayString(importedNeverHit.value.total) + " 条 · 其中 " + vue.toDisplayString(importedNeverHit.value.neverHit) + " 条暂未命中。", 1)),
_cache[116] || (_cache[116] = vue.createElementVNode("div", { class: "cap-mute" }, "「命中」只表示题目对上了号,不表示答案真的用上了。这个数只作参考:刚命中的最多一分钟后才计入,关页面太快就永远不计;暂未命中里既有你还没做到的题,也可能有题面对不上的。", -1))
], 64)) : vue.createCommentVNode("", true),
vue.withDirectives(vue.createElementVNode("input", {
class: "in",
"onUpdate:modelValue": _cache[12] || (_cache[12] = ($event) => cacheQuery.value = $event),
placeholder: "搜索题干或答案"
}, null, 512), [
[vue.vModelText, cacheQuery.value]
]),
cacheNote.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_137, vue.toDisplayString(cacheNote.value), 1)) : vue.createCommentVNode("", true),
matchedCache.value.length > CACHE_LIST_LIMIT ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_138, "共 " + vue.toDisplayString(matchedCache.value.length) + " 条 · 只列出前 " + vue.toDisplayString(CACHE_LIST_LIMIT) + " 条,用搜索缩小范围。", 1)) : vue.createCommentVNode("", true),
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(filteredCache.value, (entry) => {
return vue.openBlock(), vue.createElementBlock("div", {
key: entry.unitHash,
class: "ent"
}, [
vue.createElementVNode("div", _hoisted_139, [
vue.createElementVNode("span", _hoisted_140, vue.toDisplayString(entry.stem ? vue.unref(harvestTypeLabel)(entry.itemType) : "无题面"), 1),
entry.importedAt ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_141, "导入")) : entry.platform ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_142, vue.toDisplayString(entry.platform), 1)) : vue.createCommentVNode("", true),
vue.createElementVNode("span", _hoisted_143, vue.toDisplayString(cacheDate(entry.savedAt)), 1),
vue.createElementVNode("button", {
class: "ent-del",
"aria-label": `删除缓存 ${entry.unitHash.slice(0, 8)}`,
onClick: ($event) => removeCacheEntry(entry.unitHash)
}, [..._cache[117] || (_cache[117] = [
vue.createElementVNode("svg", {
class: "ic sm",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "1.7"
}, [
vue.createElementVNode("path", {
d: "M6 7h12M9.5 7V5.5h5V7M8 7l.7 12h6.6L16 7",
"stroke-linecap": "round",
"stroke-linejoin": "round"
})
], -1)
])], 8, _hoisted_144)
]),
vue.createElementVNode("div", {
class: vue.normalizeClass(["ent-q", { "cap-mute": !entry.stem }])
}, vue.toDisplayString(entry.stem || "这条记录没有题面(来源未提供),仍可正常命中"), 3),
vue.createElementVNode("div", _hoisted_145, vue.toDisplayString(entry.values.join("、")), 1),
entry.options.length ? (vue.openBlock(), vue.createElementBlock("details", _hoisted_146, [
vue.createElementVNode("summary", _hoisted_147, "选项 " + vue.toDisplayString(entry.options.length) + " 项", 1),
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(entry.options, (op, oi) => {
return vue.openBlock(), vue.createElementBlock("div", {
key: oi,
class: "cap-mute"
}, vue.toDisplayString(letter2(oi)) + "、" + vue.toDisplayString(op), 1);
}), 128))
])) : vue.createCommentVNode("", true)
]);
}), 128)),
!filteredCache.value.length ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_148, "没有匹配的缓存。")) : vue.createCommentVNode("", true)
], 64))
], 64)) : tab.value === "system" && systemSub.value === "diag" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 6 }, [
vue.createElementVNode("div", _hoisted_149, [
vue.createElementVNode("div", _hoisted_150, [
_cache[119] || (_cache[119] = vue.createElementVNode("span", { class: "ctitle" }, "当前规则", -1)),
ruleDiag.value ? (vue.openBlock(), vue.createElementBlock("span", {
key: 0,
class: vue.normalizeClass(["tag", ruleDiag.value.source === "remote-active" ? "acc" : "neutral"])
}, vue.toDisplayString(ruleDiag.value.sourceLabel), 3)) : vue.createCommentVNode("", true)
]),
ruleDiag.value ? (vue.openBlock(), vue.createElementBlock("dl", _hoisted_151, [
_cache[120] || (_cache[120] = vue.createElementVNode("dt", null, "规则包", -1)),
vue.createElementVNode("dd", null, vue.toDisplayString(ruleDiag.value.packageId), 1),
_cache[121] || (_cache[121] = vue.createElementVNode("dt", null, "版本", -1)),
vue.createElementVNode("dd", null, vue.toDisplayString(ruleDiag.value.version) + " · seq " + vue.toDisplayString(ruleDiag.value.releaseSequence), 1),
ruleDiag.value.release ? (vue.openBlock(), vue.createElementBlock("dt", _hoisted_152, "通道")) : vue.createCommentVNode("", true),
ruleDiag.value.release ? (vue.openBlock(), vue.createElementBlock("dd", _hoisted_153, vue.toDisplayString(ruleDiag.value.release.channel) + " · " + vue.toDisplayString(ruleDiag.value.release.rolloutPercent) + "%", 1)) : vue.createCommentVNode("", true),
_cache[122] || (_cache[122] = vue.createElementVNode("dt", null, "校验", -1)),
vue.createElementVNode("dd", null, vue.toDisplayString(ruleDiag.value.loadStatusLabel), 1)
])) : (vue.openBlock(), vue.createElementBlock("div", _hoisted_154, "当前会话暂无已匹配的 JSON 规则。")),
lastCaptureFailure.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_155, "规则捕获失败 · " + vue.toDisplayString(lastCaptureFailure.value) + " · 这页不是没有题,是规则没跑完", 1)) : vue.createCommentVNode("", true)
]),
vue.createElementVNode("div", _hoisted_156, [
vue.createElementVNode("div", { class: "row" }, [
_cache[123] || (_cache[123] = vue.createElementVNode("span", { class: "ctitle" }, "运行日志", -1)),
vue.createElementVNode("button", {
class: "btn ghost sm",
onClick: clearLogs
}, "清空")
]),
vue.createElementVNode("div", _hoisted_157, [
(vue.openBlock(), vue.createElementBlock(vue.Fragment, null, vue.renderList(LOG_LEVELS, (lvl) => {
return vue.createElementVNode("button", {
key: lvl.k,
class: vue.normalizeClass(["seg", { active: logFilter.value === lvl.k }]),
onClick: ($event) => logFilter.value = lvl.k
}, vue.toDisplayString(lvl.l), 11, _hoisted_158);
}), 64))
]),
filteredLogs.value.length ? (vue.openBlock(), vue.createElementBlock("ul", _hoisted_159, [
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(filteredLogs.value, (item, i) => {
return vue.openBlock(), vue.createElementBlock("li", {
key: i,
class: vue.normalizeClass(["log-row", `log-${item.type}`])
}, [
vue.createElementVNode("span", _hoisted_160, vue.toDisplayString(item.time), 1),
vue.createElementVNode("span", _hoisted_161, [
vue.createTextVNode(vue.toDisplayString(item.content), 1),
item.repeat > 1 ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_162, " × " + vue.toDisplayString(item.repeat), 1)) : vue.createCommentVNode("", true)
])
], 2);
}), 128))
])) : (vue.openBlock(), vue.createElementBlock("div", _hoisted_163, "暂无日志"))
]),
vue.createElementVNode("div", _hoisted_164, [
vue.createElementVNode("button", {
class: "fold",
onClick: _cache[13] || (_cache[13] = ($event) => diagOpen.value = !diagOpen.value)
}, [
_cache[125] || (_cache[125] = vue.createTextVNode("页面诊断 · dry-run 不扣分", -1)),
(vue.openBlock(), vue.createElementBlock("svg", {
class: vue.normalizeClass(["ic sm chev", { right: !diagOpen.value }])
}, [..._cache[124] || (_cache[124] = [
vue.createElementVNode("use", { href: "#i-chevron" }, null, -1)
])], 2))
]),
diagOpen.value ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
vue.createElementVNode("button", {
class: "btn ghost sm",
disabled: running.value,
onClick: runDiag
}, "运行诊断", 8, _hoisted_165),
diag.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_166, [
vue.createTextVNode(vue.toDisplayString(diag.value.matched ? `命中${platformLabel.value} · 抓到 ${diag.value.count} 题 · 图片 ${diag.value.imageCount} 张 · 收录 ${diag.value.harvestedCount} 题` : "未命中当前页") + " ", 1),
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(diag.value.items, (it, i) => {
return vue.openBlock(), vue.createElementBlock("div", { key: i }, vue.toDisplayString(i + 1) + ". [" + vue.toDisplayString(it.type) + "] " + vue.toDisplayString(it.decodeFailed ? "解码失败" : it.stemPreview) + " · " + vue.toDisplayString(it.optionCount) + " 选项", 1);
}), 128))
])) : (vue.openBlock(), vue.createElementBlock("div", _hoisted_167, "点「运行诊断」识别当前页"))
], 64)) : vue.createCommentVNode("", true),
vue.createElementVNode("button", {
class: "fold",
onClick: _cache[14] || (_cache[14] = ($event) => ruleMetaOpen.value = !ruleMetaOpen.value)
}, [
_cache[127] || (_cache[127] = vue.createTextVNode("规则明细", -1)),
(vue.openBlock(), vue.createElementBlock("svg", {
class: vue.normalizeClass(["ic sm chev", { right: !ruleMetaOpen.value }])
}, [..._cache[126] || (_cache[126] = [
vue.createElementVNode("use", { href: "#i-chevron" }, null, -1)
])], 2))
]),
ruleMetaOpen.value && ruleDiag.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_168, [
vue.createElementVNode("div", _hoisted_169, [
_cache[128] || (_cache[128] = vue.createElementVNode("span", { class: "rule-key" }, "hash", -1)),
vue.createElementVNode("span", _hoisted_170, vue.toDisplayString(ruleDiag.value.contentHash), 1)
]),
ruleDiag.value.release ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_171, [
_cache[129] || (_cache[129] = vue.createElementVNode("span", { class: "rule-key" }, "release", -1)),
vue.createElementVNode("span", _hoisted_172, [
vue.createTextVNode(vue.toDisplayString(ruleDiag.value.release.releaseId) + " · bucket " + vue.toDisplayString(ruleDiag.value.release.cohortBucket), 1),
vue.unref(isRuleCandidateTestDelivery)(ruleDiag.value.release) ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
vue.createTextVNode(" · 测试设备固定命中")
], 64)) : vue.createCommentVNode("", true)
])
])) : vue.createCommentVNode("", true),
ruleDiag.value.candidateVersion ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_173, [
_cache[130] || (_cache[130] = vue.createElementVNode("span", { class: "rule-key" }, "candidate", -1)),
vue.createElementVNode("span", _hoisted_174, vue.toDisplayString(ruleDiag.value.candidateVersion), 1)
])) : vue.createCommentVNode("", true),
ruleDiag.value.lastKnownGoodVersion ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_175, [
_cache[131] || (_cache[131] = vue.createElementVNode("span", { class: "rule-key" }, "LKG", -1)),
vue.createElementVNode("span", _hoisted_176, vue.toDisplayString(ruleDiag.value.lastKnownGoodVersion), 1)
])) : vue.createCommentVNode("", true)
])) : ruleMetaOpen.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_177, "当前会话暂无已匹配的 JSON 规则。")) : vue.createCommentVNode("", true)
])
], 64)) : vue.createCommentVNode("", true)
]),
vue.createElementVNode("div", _hoisted_178, [
tab.value === "home" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
detectedCount.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_179, [
vue.createElementVNode("div", _hoisted_180, [
_cache[132] || (_cache[132] = vue.createElementVNode("span", { class: "k" }, "将回填", -1)),
vue.createElementVNode("b", null, "最多 " + vue.toDisplayString(detectedCount.value) + " 题", 1)
]),
vue.createElementVNode("div", _hoisted_181, [
_cache[133] || (_cache[133] = vue.createElementVNode("span", { class: "k" }, "预计扣分", -1)),
vue.createElementVNode("b", null, "≤ " + vue.toDisplayString(detectedCount.value) + " 分", 1),
_cache[134] || (_cache[134] = vue.createElementVNode("span", { class: "cap-mute" }, "命中才扣", -1))
]),
_cache[135] || (_cache[135] = vue.createElementVNode("div", { class: "prow" }, [
vue.createElementVNode("span", { class: "k" }, "不会做"),
vue.createElementVNode("span", null, "提交试卷 · 未命中不写入")
], -1))
])) : vue.createCommentVNode("", true),
detectedCount.value && hasFeature("answer") ? (vue.openBlock(), vue.createElementBlock("button", {
key: 1,
class: "btn block",
disabled: running.value,
onClick: start
}, "开始答题", 8, _hoisted_182)) : (vue.openBlock(), vue.createElementBlock("button", {
key: 2,
class: "btn ghost block",
disabled: running.value,
onClick: _cache[15] || (_cache[15] = ($event) => detectQuestions())
}, "重新识别本页", 8, _hoisted_183))
], 64)) : tab.value === "ask" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [
list.value.length ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_184, [
vue.createElementVNode("span", _hoisted_185, vue.toDisplayString(tip.value), 1),
vue.createElementVNode("div", _hoisted_186, [
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(list.value, (it, i) => {
return vue.openBlock(), vue.createElementBlock("i", {
key: i,
class: vue.normalizeClass({ on: it.status !== "pending" })
}, null, 2);
}), 128))
])
])) : vue.createCommentVNode("", true),
hasFeature("answer") ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_187, [
vue.createElementVNode("button", {
class: "btn",
style: { "flex": "1" },
disabled: running.value,
onClick: start
}, "开始答题", 8, _hoisted_188),
running.value ? (vue.openBlock(), vue.createElementBlock("button", {
key: 0,
class: "btn ghost",
style: { "flex": "1" },
onClick: pause
}, "暂停")) : (vue.openBlock(), vue.createElementBlock("button", {
key: 1,
class: "btn ghost",
style: { "flex": "1" },
onClick: restart
}, "重新答题"))
])) : vue.createCommentVNode("", true)
], 64)) : tab.value === "system" && systemSub.value === "cache" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 2 }, [
cacheImportPreview.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_189, [
vue.createElementVNode("button", {
class: "btn ghost",
style: { "flex": "1" },
onClick: cancelImport
}, "取消"),
vue.createElementVNode("button", {
class: "btn",
style: { "flex": "2" },
onClick: confirmImport
}, "导入 " + vue.toDisplayString(cacheImportPreview.value.fileCount) + " 条", 1)
])) : cacheClearPending.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_190, [
vue.createElementVNode("button", {
class: "btn ghost",
style: { "flex": "1" },
onClick: _cache[16] || (_cache[16] = ($event) => cacheClearPending.value = false)
}, "取消"),
vue.createElementVNode("button", {
class: "btn ghost danger",
style: { "flex": "2" },
onClick: clearCacheAll
}, "确认清空")
])) : cacheEntries.value.length ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_191, [
vue.createElementVNode("button", {
class: "btn ghost",
style: { "flex": "1" },
onClick: exportCache
}, "导出"),
vue.createElementVNode("button", {
class: "btn ghost",
style: { "flex": "1" },
onClick: pickImportFile
}, "文件导入"),
vue.createElementVNode("a", {
class: "btn ghost",
style: { "flex": "1" },
href: PARSE_IMPORT_URL,
target: "_blank",
rel: "noopener noreferrer"
}, "解析导入"),
vue.createElementVNode("button", {
class: "btn ghost danger",
onClick: _cache[17] || (_cache[17] = ($event) => cacheClearPending.value = true)
}, "清空")
])) : vue.createCommentVNode("", true)
], 64)) : tab.value === "system" && systemSub.value === "diag" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 3 }, [
vue.createElementVNode("button", {
class: "btn ghost block",
onClick: exportDiagnostics
}, "导出诊断(日志 + 规则信息)"),
vue.unref(IS_DEV) ? (vue.openBlock(), vue.createElementBlock("button", {
key: 0,
class: "btn ghost danger block",
disabled: ruleUpdating.value,
onClick: resetRuleStorageAndReload
}, " 重置规则数据并刷新(dev) ", 8, _hoisted_192)) : vue.createCommentVNode("", true)
], 64)) : vue.createCommentVNode("", true),
vue.createElementVNode("div", _hoisted_193, [
vue.createElementVNode("span", _hoisted_194, "v" + vue.toDisplayString(vue.unref(SCRIPT_VERSION)) + " · " + vue.toDisplayString(ruleVersionLabel.value), 1),
_cache[136] || (_cache[136] = vue.createElementVNode("span", { class: "luokuan" }, "问,必有答。", -1))
])
]),
accountOpen.value ? (vue.openBlock(), vue.createElementBlock("div", {
key: 1,
class: "scrim",
onClick: closeAccount
})) : vue.createCommentVNode("", true),
accountOpen.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_195, [
!loggedIn.value ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
_cache[137] || (_cache[137] = vue.createElementVNode("div", { class: "ctitle" }, "登录", -1)),
vue.withDirectives(vue.createElementVNode("input", {
class: "in",
"onUpdate:modelValue": _cache[18] || (_cache[18] = ($event) => username.value = $event),
placeholder: "用户名"
}, null, 512), [
[vue.vModelText, username.value]
]),
vue.withDirectives(vue.createElementVNode("input", {
class: "in",
"onUpdate:modelValue": _cache[19] || (_cache[19] = ($event) => password.value = $event),
type: "password",
placeholder: "密码",
onKeyup: _cache[20] || (_cache[20] = vue.withKeys(($event) => doAuth("login"), ["enter"]))
}, null, 544), [
[vue.vModelText, password.value]
]),
vue.withDirectives(vue.createElementVNode("input", {
class: "in",
"onUpdate:modelValue": _cache[21] || (_cache[21] = ($event) => email.value = $event),
type: "email",
placeholder: "邮箱 选填,注册时用于找回密码"
}, null, 512), [
[vue.vModelText, email.value]
]),
vue.createElementVNode("div", _hoisted_196, [
vue.createElementVNode("button", {
class: "btn",
style: { "flex": "1" },
disabled: authing.value,
onClick: _cache[22] || (_cache[22] = ($event) => doAuth("login"))
}, "登录", 8, _hoisted_197),
vue.createElementVNode("button", {
class: "btn ghost",
style: { "flex": "1" },
disabled: authing.value,
onClick: _cache[23] || (_cache[23] = ($event) => doAuth("register"))
}, "注册", 8, _hoisted_198)
]),
authMsg.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_199, vue.toDisplayString(authMsg.value), 1)) : vue.createCommentVNode("", true),
_cache[138] || (_cache[138] = vue.createElementVNode("div", { class: "cap-mute" }, "注册要求用户名 3-32 位、密码至少 8 位;登录不受此限,老账号照原样填。", -1)),
_cache[139] || (_cache[139] = vue.createElementVNode("div", { class: "cap-mute" }, "邮箱不填也能注册。不填则忘记密码后无法找回。", -1)),
_cache[140] || (_cache[140] = vue.createElementVNode("div", { class: "cap-mute" }, "未登录时仅查询免费题库。", -1))
], 64)) : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [
vue.createElementVNode("div", _hoisted_200, [
vue.createElementVNode("span", _hoisted_201, vue.toDisplayString(avatarInitial.value), 1),
vue.createElementVNode("div", _hoisted_202, [
vue.createElementVNode("div", _hoisted_203, vue.toDisplayString(accountName.value || "已登录"), 1),
authStale.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_204, vue.toDisplayString(AUTH_STALE_NOTE))) : vue.createCommentVNode("", true)
])
]),
authStale.value ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
vue.withDirectives(vue.createElementVNode("input", {
class: "in",
"onUpdate:modelValue": _cache[24] || (_cache[24] = ($event) => password.value = $event),
type: "password",
placeholder: "密码",
onKeyup: _cache[25] || (_cache[25] = vue.withKeys(($event) => doAuth("login"), ["enter"]))
}, null, 544), [
[vue.vModelText, password.value]
]),
vue.createElementVNode("button", {
class: "btn",
disabled: authing.value,
onClick: _cache[26] || (_cache[26] = ($event) => doAuth("login"))
}, "重新登录", 8, _hoisted_205),
authMsg.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_206, vue.toDisplayString(authMsg.value), 1)) : vue.createCommentVNode("", true)
], 64)) : vue.createCommentVNode("", true),
vue.createElementVNode("div", _hoisted_207, [
_cache[141] || (_cache[141] = vue.createElementVNode("span", { class: "lbl" }, "积分余额", -1)),
balance.value != null ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_208, [
vue.createElementVNode("span", _hoisted_209, vue.toDisplayString(balance.value), 1)
])) : (vue.openBlock(), vue.createElementBlock("span", _hoisted_210, "读取中…"))
]),
emailBound.value === false ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_211, " 这个账号没有绑定邮箱,忘记密码后无法自助找回。 ")) : vue.createCommentVNode("", true),
vue.createElementVNode("div", _hoisted_212, [
vue.withDirectives(vue.createElementVNode("input", {
class: "in",
"onUpdate:modelValue": _cache[27] || (_cache[27] = ($event) => cardCode.value = $event),
placeholder: "输入卡密",
onKeyup: vue.withKeys(doRedeem, ["enter"])
}, null, 544), [
[vue.vModelText, cardCode.value]
]),
vue.createElementVNode("button", {
class: "btn",
disabled: !cardCode.value.trim() || redeeming.value,
onClick: doRedeem
}, vue.toDisplayString(redeeming.value ? "兑换中…" : "兑换"), 9, _hoisted_213)
]),
redeemNote.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_214, vue.toDisplayString(redeemNote.value), 1)) : vue.createCommentVNode("", true),
_cache[142] || (_cache[142] = vue.createElementVNode("div", { class: "cap-mute" }, "命中才计分,未命中不扣分;同一题重跑不重复扣分。", -1)),
vue.createElementVNode("div", _hoisted_215, [
vue.createElementVNode("span", _hoisted_216, vue.toDisplayString(accountName.value), 1),
vue.createElementVNode("button", {
class: "btn danger sm",
onClick: logout
}, "退出登录")
])
], 64))
])) : vue.createCommentVNode("", true),
captchaOpen.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_217, [
vue.createElementVNode("div", _hoisted_218, [
vue.createElementVNode("div", { class: "row" }, [
_cache[144] || (_cache[144] = vue.createElementVNode("span", { class: "ctitle" }, "完成人机验证", -1)),
vue.createElementVNode("button", {
class: "x",
type: "button",
"aria-label": "取消人机验证",
onClick: cancelCaptcha
}, [..._cache[143] || (_cache[143] = [
vue.createElementVNode("svg", { class: "ic" }, [
vue.createElementVNode("use", { href: "#i-minus" })
], -1)
])])
]),
vue.createElementVNode("iframe", {
ref_key: "captchaFrame",
ref: captchaFrame,
class: "captcha-frame",
src: captchaUrl,
title: "爱问答注册人机验证",
sandbox: "allow-scripts allow-same-origin",
onLoad: onCaptchaFrameLoad
}, null, 544),
_cache[145] || (_cache[145] = vue.createElementVNode("div", { class: "cap-mute" }, "验证结果只随加密注册请求发送。", -1))
])
])) : vue.createCommentVNode("", true)
], 4));
};
}
});
const PANEL_STYLE = `
:host, .aiask-root {
--acc: #1e478f;
--acc-tint: color-mix(in srgb, var(--acc) 9%, #fff);
--ink: #171a21; --body: #4b5059; --mute: #8b909b;
--line: #e6e8ec; --line-strong: #aab0ba;
--canvas: #fff; --soft: #f6f7f9;
--err: #c8322f;
--mono: "JetBrains Mono","IBM Plex Mono","Geist Mono",ui-monospace,SFMono-Regular,Menlo,monospace;
--sans: "Inter","Geist",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
--zhu: #c7391b;
--serif: "Songti SC","Noto Serif SC","SimSun",serif;
font-family: var(--sans);
font-feature-settings: "ss01","ss02","cv01","tnum";
font-variant-numeric: tabular-nums;
color: var(--ink); -webkit-font-smoothing: antialiased;
/* 长 token(latin 用户名/规则包 id/选项里的 URL)兜底断行;nowrap 元素不受影响 */
overflow-wrap: anywhere;
}
.aiask-root * { box-sizing: border-box; }
/* 折叠气泡:右下角,不抢戏;整块是可拖拽触控面 */
.bubble { position: fixed; right: 16px; bottom: 16px; z-index: 2147483647; display: flex; align-items: center; gap: 8px; cursor: move; user-select: none; touch-action: none; }
.tip { background: var(--canvas); border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; color: var(--body); font: 12px/1.3 var(--mono); box-shadow: 0 1px 2px rgba(23,26,33,.05); }
.launcher { appearance: none; position: relative; width: 44px; height: 44px; border: 0; background: transparent; cursor: move; padding: 0; display: flex; align-items: center; justify-content: center; box-shadow: none; }
.badge { position: absolute; top: -5px; right: -5px; min-width: 18px; height: 18px; padding: 0 4px; border-radius: 9px; background: var(--acc); color: #fff; font: 11px/18px var(--mono); text-align: center; border: 2px solid var(--canvas); box-sizing: border-box; }
/* 朱印:品牌签名,只出现在 logo / 落款 / 完成印三处 */
.seal { background: var(--zhu); color: #fff; display: flex; align-items: center; justify-content: center; font-family: var(--serif); font-weight: 700; flex: 0 0 auto; box-shadow: inset 0 0 0 1px rgba(255,255,255,.55); }
.seal.s44 { width: 44px; height: 44px; border-radius: 8px; font-size: 26px; box-shadow: inset 0 0 0 1.5px rgba(255,255,255,.55), 0 2px 6px rgba(23,26,33,.18); }
.seal.s28 { width: 28px; height: 28px; border-radius: 5px; font-size: 17px; }
.seal.s22 { width: 22px; height: 22px; border-radius: 4px; font-size: 14px; }
/* 落款:slogan 的唯一常驻位(动作条页脚右侧) */
.luokuan { font-family: var(--serif); color: var(--zhu); font-size: 11px; letter-spacing: 1px; }
/* 待命态标语:宋体、宽字距 */
.standby-title { font-family: var(--serif); font-size: 17px; letter-spacing: 6px; color: var(--ink); }
.standby { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 22px 0 10px; text-align: center; }
/* 完成印:全脚本唯一装饰动效 */
.done-seal { position: absolute; right: 10px; top: 9px; width: 48px; height: 48px; border-radius: 9px; background: var(--zhu); color: #fff; display: flex; align-items: center; justify-content: center; font-family: var(--serif); font-weight: 700; font-size: 28px; opacity: .92; box-shadow: inset 0 0 0 2px rgba(255,255,255,.5), 0 1px 3px rgba(199,57,27,.3); animation: seal-drop .18s cubic-bezier(.22,1,.36,1) both; }
@keyframes seal-drop {
from { transform: scale(1.15) rotate(0deg); opacity: 0; }
to { transform: scale(1) rotate(-4deg); opacity: .92; }
}
@media (prefers-reduced-motion: reduce) {
.done-seal { animation: none; transform: rotate(-4deg); }
}
/* 面板外壳:唯一被托起的浮层,一道克制冷调堆叠阴影抬离宿主页;radius 6 一档 */
.panel { position: fixed; right: 16px; bottom: 16px; z-index: 2147483647; width: min(340px, calc(100vw - 32px)); background: var(--canvas); border: 1px solid var(--line); border-radius: 6px; overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 1px 1px rgba(23,26,33,.03), 0 6px 14px -4px rgba(23,26,33,.05), 0 20px 30px -12px rgba(23,26,33,.10); }
/* header:固定槽位契约 墨标 · name · spacer · [可变 chip] · 收起 */
.head { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--line); cursor: move; user-select: none; touch-action: none; }
.name { font-size: 13.5px; font-weight: 600; letter-spacing: -0.3px; white-space: nowrap; }
.spacer { flex: 1; }
.chip { font: 12px/1.3 var(--sans); color: var(--body); background: var(--soft); border: 1px solid var(--line); border-radius: 6px; padding: 2px 8px; white-space: nowrap; max-width: 120px; overflow: hidden; text-overflow: ellipsis; }
.chip.mono { font-family: var(--mono); }
.x { border: 1px solid transparent; background: none; cursor: pointer; color: var(--mute); width: 22px; height: 22px; border-radius: 6px; display: flex; align-items: center; justify-content: center; flex: 0 0 auto; padding: 0; }
.x:hover { background: var(--soft); color: var(--ink); }
/* header 头像:账户唯一入口 */
.ava { width: 24px; height: 24px; border-radius: 5px; background: var(--ink); color: #fff; font-size: 12px; font-weight: 600; display: flex; align-items: center; justify-content: center; border: none; padding: 0; cursor: pointer; flex: 0 0 auto; font-family: var(--sans); }
.ava.out { background: var(--canvas); color: var(--mute); border: 1px dashed var(--line-strong); font-weight: 400; }
.ava.lg { width: 32px; height: 32px; border-radius: 6px; font-size: 15px; }
/* 账户弹层:锚定 header 右下,scrim 点击外部关闭 */
.scrim { position: absolute; inset: 0; z-index: 8; background: rgba(23,26,33,.10); }
.pop { position: absolute; top: 40px; right: 10px; width: 262px; z-index: 9; background: var(--canvas); border: 1px solid var(--line); border-radius: 8px; padding: 12px; display: flex; flex-direction: column; gap: 10px; box-shadow: 0 1px 2px rgba(23,26,33,.04), 0 10px 20px -6px rgba(23,26,33,.14); }
.sep-top { border-top: 1px solid var(--line); padding-top: 9px; }
/* TabBar:选中 = ink 文字 + ink 下划线(chrome 回落墨色,强调色不碰 chrome) */
.tabbar { display: flex; gap: 2px; padding: 0 8px; border-bottom: 1px solid var(--line); flex: 0 0 auto; }
.tab { appearance: none; border: none; background: none; cursor: pointer; padding: 8px 10px; font-size: 13px; letter-spacing: -0.2px; color: var(--body); border-bottom: 2px solid transparent; margin-bottom: -1px; }
.tab.active { color: var(--ink); font-weight: 600; border-bottom-color: var(--ink); }
/* 系统子段:设置 | 缓存 | 诊断(对齐旧 SegmentedControl,密度克制) */
.subbar { display: flex; gap: 4px; padding: 8px 12px 0; flex: 0 0 auto; }
.seg { appearance: none; border: 1px solid var(--line); background: var(--canvas); color: var(--body); cursor: pointer; padding: 4px 10px; font-size: 12px; border-radius: 6px; font-family: var(--sans); line-height: 1.3; }
.seg.active { background: var(--ink); color: #fff; border-color: var(--ink); }
.seg:hover:not(.active) { background: var(--soft); }
/* 首页 / 账户身份条 */
.home-user { display: flex; align-items: center; gap: 8px; }
.home-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/* 运行日志列表 */
.log-filter { display: flex; flex-wrap: wrap; gap: 4px; }
.log-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; max-height: 200px; overflow-y: auto; }
.log-row { display: flex; gap: 8px; align-items: flex-start; font-size: 12px; line-height: 1.4; padding: 4px 0; border-bottom: 1px solid var(--line); }
.log-row:last-child { border-bottom: none; }
.log-time { color: var(--mute); flex: 0 0 auto; }
.log-repeat { color: var(--muted); }
.log-msg { color: var(--body); flex: 1; min-width: 0; word-break: break-word; }
.log-row.log-warning .log-msg { color: var(--body); }
.log-row.log-error .log-msg { color: var(--err); }
/* 规则诊断:技术信息只用 mono 与 hairline,不引入第二强调色。 */
.rule-meta { border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
.rule-row { display: grid; grid-template-columns: 70px minmax(0, 1fr); gap: 8px; padding: 6px 8px; border-bottom: 1px solid var(--line); font: 11.5px/1.45 var(--mono); }
.rule-row:last-child { border-bottom: none; }
.rule-key { color: var(--mute); }
.rule-value { color: var(--body); overflow-wrap: anywhere; }
/* body 两级节奏:组间 12 / 组内 8;只保留纵向滚动;动作条固定后由 flex 分配高度 */
.body { padding: 12px; display: flex; flex-direction: column; gap: 12px; flex: 1 1 auto; min-height: 0; max-height: min(520px, calc(100vh - 200px)); overflow-x: hidden; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }
.body::-webkit-scrollbar { width: 6px; }
.body::-webkit-scrollbar-track { background: transparent; }
.body::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 6px; }
.grp { display: flex; flex-direction: column; gap: 8px; }
.sep { border-top: 1px solid var(--line); }
/* 分组小标:系统页唯一的小节标记 */
.gh2 { font: 11px/1.4 var(--mono); color: var(--mute); letter-spacing: .3px; }
/* 诊断状态卡:一眼看完规则状态 */
.statcard { border: 1px solid var(--line); border-radius: 6px; padding: 11px; display: flex; flex-direction: column; gap: 8px; }
.statgrid { display: grid; grid-template-columns: auto 1fr; gap: 5px 10px; font-size: 12px; align-items: baseline; margin: 0; }
.statgrid dt { color: var(--mute); }
.statgrid dd { margin: 0; font-family: var(--mono); }
/* 底部动作条:主行动与关键读数恒定可见 */
.actbar { flex: 0 0 auto; border-top: 1px solid var(--line); background: var(--canvas); padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; }
.actbar-foot { display: flex; align-items: center; justify-content: space-between; }
/* 唯一「可折叠」标记:caption-mono + 恒显 chevron + hover 底色 + 指针 */
.fold { display: flex; align-items: center; gap: 6px; cursor: pointer; font: 12px/1.4 var(--mono); color: var(--mute); padding: 4px 6px; margin: 0 -6px; border-radius: 6px; background: none; border: none; text-align: left; width: calc(100% + 12px); }
.fold:hover { background: var(--soft); }
.fold .chev { margin-left: auto; color: var(--mute); transition: transform .15s ease; }
.fold .chev.right { transform: rotate(-90deg); }
/* 唯一真 card:hairline + padding,无阴影(编辑感 flat,深度只留给外壳) */
.card { min-width: 0; border: 1px solid var(--line); border-radius: 6px; padding: 12px; display: flex; flex-direction: column; gap: 8px; background: var(--canvas); }
/* 文字层次:每屏一个重音,其余压支持层;负字距 = 编辑声音 */
.gate-h { font-size: 16px; font-weight: 600; letter-spacing: -0.4px; color: var(--ink); }
.ctitle { font-size: 13.5px; font-weight: 600; letter-spacing: -0.2px; color: var(--ink); }
.stem { min-width: 0; font-size: 15px; font-weight: 500; letter-spacing: -0.25px; line-height: 1.45; color: var(--ink); word-break: auto-phrase; text-wrap: pretty; }
.question-content { max-width: 100%; white-space: pre-wrap; overflow-wrap: anywhere; }
.question-content img { display: inline-block; max-width: 100%; height: auto; object-fit: contain; vertical-align: middle; }
.image-failed { color: var(--err); font-size: 12px; }
.lbl { font-size: 13.5px; font-weight: 500; letter-spacing: -0.2px; }
.locator { font-size: 12px; color: var(--body); letter-spacing: -0.1px; }
.cap-mute { font-size: 12px; color: var(--mute); line-height: 1.45; }
.mono { font-family: var(--mono); }
/* 刷课卡「跳过 N 项」折叠:三角同 .ent-ops,不用浏览器原生的 disclosure 标记 */
.skip summary { cursor: pointer; list-style: none; }
.skip summary::-webkit-details-marker { display: none; }
.skip summary::before { content: '▸ '; }
.skip[open] summary::before { content: '▾ '; }
.skip div { padding-left: 12px; }
/* 图标:手作 SVG 子集,统一 1.6 描边、currentColor */
.ic { width: 14px; height: 14px; display: block; color: currentColor; flex: 0 0 auto; }
.ic.sm { width: 12px; height: 12px; }
/* 按钮:主/动作 chrome 全 ink 填充(墨黑是转化目标),高 32(sm 28) */
.btn { appearance: none; border: 1px solid var(--ink); background: var(--ink); color: #fff; border-radius: 6px; height: 32px; padding: 0 12px; font-size: 13.5px; font-weight: 500; letter-spacing: -0.2px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: 6px; line-height: 1; font-family: var(--sans); flex-shrink: 0; text-decoration: none; }
.btn:hover { background: #0f1218; border-color: #0f1218; }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn.ghost { background: var(--canvas); color: var(--ink); border-color: var(--line); }
.btn.ghost:hover:not(:disabled) { background: var(--soft); border-color: var(--line-strong); }
.btn.ghost.sub { color: var(--body); }
.btn.danger { background: var(--canvas); color: var(--err); border-color: color-mix(in srgb, var(--err) 28%, #fff); }
.btn.danger:hover { background: color-mix(in srgb, var(--err) 6%, #fff); }
.btn.sm { height: 28px; padding: 0 8px; font-size: 12px; }
.btn.block { width: 100%; }
/* 输入:height 36,placeholder 兼 label;焦点 = 2px 实线 accent,无柔光环 */
.in { width: 100%; height: 36px; padding: 0 12px; font-family: var(--sans); border: 1px solid var(--line); border-radius: 6px; font-size: 13.5px; letter-spacing: -0.2px; color: var(--ink); background: var(--canvas); }
.in::placeholder { color: var(--mute); }
.in:focus { outline: 2px solid var(--acc); outline-offset: 0; border-color: var(--acc); }
/* 注册验证:挑战页是后端 /captcha(2026-08-15 起同域 www.aiask.site)的 iframe,
自托管 Altcha 在里面跑,结果经专用 MessageChannel 回到油猴沙箱。 */
.captcha-cover { position: absolute; inset: 0; z-index: 20; display: grid; place-items: center; padding: 12px; background: color-mix(in srgb, var(--canvas) 94%, transparent); }
.captcha-card { width: 100%; padding: 12px; display: flex; flex-direction: column; gap: 8px; border: 1px solid var(--line); border-radius: 6px; background: var(--canvas); box-shadow: 0 1px 1px rgba(23,26,33,.03), 0 8px 16px -4px rgba(23,26,33,.08); }
.captcha-frame { display: block; width: 100%; height: 150px; border: 1px solid var(--line); border-radius: 6px; background: var(--canvas); }
/* 出手前预演:花钱或改写本地数据前显示后果 */
.prev { border: 1px solid var(--line-strong); border-radius: 6px; background: var(--soft); padding: 9px 10px; display: flex; flex-direction: column; gap: 5px; }
.prow { display: flex; align-items: baseline; gap: 6px; font-size: 12px; }
.prow .k { color: var(--mute); min-width: 56px; flex: 0 0 auto; }
.prow b { font-family: var(--mono); font-weight: 600; }
.done { position: relative; display: flex; flex-direction: column; gap: 8px; }
/* 控制条 */
.toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* 进度:12 段 ticked bar(呼应题号格);填充 = ink(扫描进度是 chrome,不吃 accent) */
.prog { display: flex; align-items: center; gap: 8px; }
.prog .stat { font-size: 12px; color: var(--body); white-space: nowrap; }
.prog .stat b { font-family: var(--mono); font-weight: 400; color: var(--ink); }
.ticks { flex: 1; display: flex; gap: 2px; height: 6px; }
.ticks i { flex: 1; border-radius: 1px; background: var(--line); }
.ticks i.on { background: var(--ink); }
/* Switch:开态 track = ink(不吃 accent),off = line-strong;胶囊仅留给真 toggle */
.switch-row { display: flex; align-items: center; gap: 8px; }
.switch { width: 32px; height: 20px; border-radius: 999px; background: var(--ink); position: relative; flex: 0 0 auto; border: none; cursor: pointer; padding: 0; }
.switch.off { background: var(--line-strong); }
.switch > i { position: absolute; top: 2px; left: 14px; width: 16px; height: 16px; border-radius: 50%; background: #fff; box-shadow: 0 1px 1px rgba(23,26,33,.2); transition: left .15s ease; }
.switch.off > i { left: 2px; }
/* Tag:6px 方角(非胶囊) */
.tag { display: inline-flex; align-items: center; font-size: 12px; padding: 2px 8px; border-radius: 6px; line-height: 1.4; white-space: nowrap; }
.tag.acc { background: var(--acc-tint); color: var(--acc); border: 1px solid color-mix(in srgb, var(--acc) 22%, #fff); font-family: var(--mono); }
.tag.neutral { background: var(--soft); color: var(--body); border: 1px solid var(--line); }
/* Banner:仅可恢复阻塞态 */
.banner { display: flex; align-items: center; gap: 8px; border-radius: 6px; padding: 8px 12px; font-size: 13px; background: var(--soft); border: 1px solid var(--line); color: var(--body); }
/* 题号网格:当前环 + 已答/未答/无答案(无答案=红字号) */
.legend { display: flex; gap: 8px 12px; flex-wrap: wrap; }
.legend span { display: flex; align-items: center; gap: 4px; font-size: 12px; color: var(--mute); white-space: nowrap; }
.sw { width: 10px; height: 10px; border-radius: 2px; background: var(--canvas); border: 1px solid var(--line); flex: 0 0 auto; }
.sw.cur { box-shadow: inset 0 0 0 2px var(--acc); border-color: transparent; }
.sw.hit { background: var(--acc-tint); border-color: var(--acc); }
.sw.miss { background: color-mix(in srgb, var(--err) 10%, #fff); border-color: var(--err); }
.grid { display: flex; flex-wrap: wrap; gap: 4px; }
.cell { width: 22px; height: 22px; border: 1px solid var(--line); border-radius: 6px; background: var(--canvas); cursor: pointer; font: 12px/1 var(--mono); color: var(--body); padding: 0; display: flex; align-items: center; justify-content: center; }
.cell.cur { box-shadow: inset 0 0 0 2px var(--acc); border-color: transparent; color: var(--ink); }
.cell.hit { background: var(--acc-tint); border-color: var(--acc); color: var(--acc); }
.cell.miss { color: var(--err); border-color: color-mix(in srgb, var(--err) 35%, #fff); background: color-mix(in srgb, var(--err) 6%, #fff); }
/* 选项 / 答案:默认只显命中选项 + 参考答案 + 展开选项 */
.opts { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.optrow { min-width: 0; display: flex; align-items: center; gap: 8px; }
.opt { min-width: 0; flex: 1; font-size: 13.5px; color: var(--body); line-height: 1.45; letter-spacing: -0.2px; }
.opt.hit { color: var(--acc); font-weight: 500; }
.expand { display: inline-flex; align-items: center; gap: 4px; font: 12px/1 var(--mono); color: var(--mute); cursor: pointer; white-space: nowrap; background: none; border: none; padding: 0; }
.question-head { min-height: 28px; }
.stem-type { margin-right: 4px; color: var(--mute); font-weight: 400; }
.answer-block { display: flex; flex-direction: column; gap: 8px; padding-top: 8px; border-top: 1px solid var(--line); }
.answer-label { font-size: 12px; color: var(--mute); }
.answer-value { color: var(--acc); font: 500 13px/1.5 var(--mono); word-break: break-word; }
.answer-list { display: flex; flex-direction: column; gap: 6px; }
.answer-item { display: flex; align-items: flex-start; gap: 8px; }
.answer-key { flex: 0 0 auto; min-width: 36px; color: var(--muted); font: 12px/1.5 var(--mono); }
/* 缓存条目:题干为主,答案钴蓝 mono,元信息最轻 */
.ent { border: 1px solid var(--line); border-radius: 6px; padding: 9px 10px; display: flex; flex-direction: column; gap: 5px; }
.ent-top { display: flex; align-items: center; gap: 6px; }
.ent-ty { font: 10.5px/1.4 var(--mono); color: var(--body); border: 1px solid var(--line); border-radius: 3px; padding: 1px 5px; flex: 0 0 auto; }
.ent-tm { font: 10.5px/1.4 var(--mono); color: var(--mute); margin-left: auto; }
.ent-q { font-size: 13px; line-height: 1.45; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3; overflow: hidden; }
.ent-a { font: 12.5px/1.5 var(--mono); color: var(--acc); word-break: break-word; }
.ent-ops summary { cursor: pointer; list-style: none; }
.ent-ops summary::-webkit-details-marker { display: none; }
.ent-ops summary::before { content: '▸ '; }
.ent-ops[open] summary::before { content: '▾ '; }
.ent-ops div { padding-left: 12px; }
.ent-del { width: 20px; height: 20px; border: none; background: none; color: var(--line-strong); cursor: pointer; padding: 0; display: flex; align-items: center; justify-content: center; border-radius: 4px; flex: 0 0 auto; }
.ent-del:hover { background: color-mix(in srgb, var(--err) 6%, #fff); color: var(--err); }
/* 容量计量 */
.meter { flex: 1; height: 5px; background: var(--soft); border-radius: 3px; overflow: hidden; }
.meter i { display: block; height: 100%; background: var(--ink); }
.meter i.over { background: var(--err); }
/* #68 告警条:容量超限与落盘失败两处,用 --err 而非朱磦(品牌色不做功能色) */
.alert { border: 1px solid color-mix(in srgb, var(--err) 35%, #fff); background: color-mix(in srgb, var(--err) 6%, #fff); color: var(--err); border-radius: 6px; padding: 8px 10px; font-size: 12px; line-height: 1.55; }
/* 余额快照:唯一 display 级 mono 20/600 = 账户重音 */
.row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.balance { font-size: 20px; font-weight: 600; letter-spacing: -0.6px; font-family: var(--mono); color: var(--ink); }
.range { width: 100%; accent-color: var(--ink); }
`;
function mountPanel() {
if (document.getElementById("aiask-host")) return;
const host = document.createElement("div");
host.id = "aiask-host";
document.body.appendChild(host);
const shadow = host.attachShadow({ mode: "closed" });
const style = document.createElement("style");
style.textContent = PANEL_STYLE;
shadow.appendChild(style);
const root = document.createElement("div");
root.className = "aiask-root";
shadow.appendChild(root);
vue.createApp(_sfc_main).mount(root);
}
installAopengResponseCapture(location.hostname);
if (isImportBridgePage(location)) installImportBridge(localAnswerCache);
if (SUPPORTED_HOST_PATTERN.test(location.hostname)) {
const highest = findHighestSameOriginWindow(window);
const isTop = window === window.top;
const isHighestSameOrigin = highest === window;
let ancestorOrigins = [];
try {
ancestorOrigins = Array.from(location.ancestorOrigins ?? []);
} catch {
ancestorOrigins = [];
}
const role = resolvePanelRole({
isTop,
isHighestSameOrigin,
ancestorOrigins,
supportedHostPattern: SUPPORTED_HOST_PATTERN
});
if (role === "mount") {
const run = async () => {
await initializeRuleStoreRuntime();
mountPanel();
void checkRuleUpdates();
};
if (document.readyState === "loading")
document.addEventListener("DOMContentLoaded", () => void run());
else void run();
} else if (role === "relay-f9") {
if (document.readyState === "loading")
document.addEventListener(
"DOMContentLoaded",
() => notifyFrameReady(highest)
);
else notifyFrameReady(highest);
addEventListener("keydown", (e) => {
if (e.key === "F9") {
try {
highest.document.dispatchEvent(
new KeyboardEvent("keydown", { key: "F9", bubbles: true })
);
} catch {
}
}
});
}
}
})(Vue);