1271 lines
47 KiB
JavaScript
1271 lines
47 KiB
JavaScript
(function (root, factory) {
|
||
const api = factory();
|
||
if (typeof module === "object" && module.exports) module.exports = api;
|
||
if (root) root.LskjPetRuntime = api;
|
||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||
"use strict";
|
||
|
||
const ATLAS = Object.freeze({
|
||
columns: 8,
|
||
rows: 9,
|
||
frameWidth: 192,
|
||
frameHeight: 208,
|
||
width: 1536,
|
||
height: 1872
|
||
});
|
||
|
||
const ANIMATIONS = Object.freeze({
|
||
idle: animation(0, [280, 110, 110, 140, 140, 320]),
|
||
"running-right": animation(1, [120, 120, 120, 120, 120, 120, 120, 220]),
|
||
"running-left": animation(2, [120, 120, 120, 120, 120, 120, 120, 220]),
|
||
waving: animation(3, [140, 140, 140, 280]),
|
||
jumping: animation(4, [140, 140, 140, 140, 280]),
|
||
failed: animation(5, [140, 140, 140, 140, 140, 140, 140, 240]),
|
||
waiting: animation(6, [150, 150, 150, 150, 150, 260]),
|
||
running: animation(7, [120, 120, 120, 120, 120, 220]),
|
||
review: animation(8, [150, 150, 150, 150, 150, 280])
|
||
});
|
||
|
||
const SEMANTIC_STATES = Object.freeze({
|
||
idle: "idle",
|
||
listening: "waving",
|
||
thinking: "review",
|
||
planning: "review",
|
||
awaiting_confirmation: "waiting",
|
||
executing: "running",
|
||
success: "jumping",
|
||
error: "failed",
|
||
offline: "waiting",
|
||
dragging_left: "running-left",
|
||
dragging_right: "running-right"
|
||
});
|
||
|
||
const QUICK_ACTION_PROMPTS = Object.freeze({
|
||
purchase_invoice_entry:
|
||
"我要录入采购发票或采购明细。请先读取当前 ERP 上下文和能力;如果还没有附件或完整明细,只说明需要上传或提供哪些内容,不要生成写入计划。",
|
||
leave_request:
|
||
"我要申请请假。请先读取当前 ERP 上下文和能力,再询问请假类型、原始日期表达与时段、原因以及是否需要提交审批;不得替我补全或猜测。",
|
||
current_module_help:
|
||
"当前 ERP 界面有哪些功能?请先读取实时上下文,并只根据当前模块的用户级功能说明回答。",
|
||
current_module_diagnosis:
|
||
"请对当前 ERP 界面先做只读配置诊断,说明可能缺失的低代码配置;不要复现初始化,也不要生成修复 SQL。",
|
||
workflow_readiness:
|
||
"请检查采购和请假业务能力为什么尚未启用。先读取当前 ERP 上下文和能力;只有当前登录身份是 ERP 内置管理员且能力列表包含 adapters.status 时才调用它,只依据稳定 code、message、nextAction 和 openBlockerCodes 解释,不要泄露配置证据、SQL 或物理字段。"
|
||
});
|
||
const ADMIN_QUICK_ACTIONS = Object.freeze(new Set(["workflow_readiness"]));
|
||
|
||
const MAX_PURCHASE_PREVIEW_LINES = 200;
|
||
const PURCHASE_PREVIEW_KEYS = Object.freeze([
|
||
"供应商", "发票号码", "发票日期", "币种",
|
||
"不含税金额", "税额", "价税合计", "来源附件",
|
||
"明细汇总不含税", "明细汇总税额", "明细汇总价税",
|
||
"发票行数", "确定匹配行数", "来源采购单", "来源汇率",
|
||
"重复发票"
|
||
]);
|
||
const LEAVE_CREATE_PREVIEW_KEYS = Object.freeze([
|
||
"员工", "请假类型", "流转类别", "开始时间", "结束时间",
|
||
"核算工时", "原因", "创建后提交"
|
||
]);
|
||
const LEAVE_SUBMIT_PREVIEW_KEYS = Object.freeze(["申请编号", "动作"]);
|
||
const INITIALIZATION_TRACE_PREVIEW_KEYS = Object.freeze([
|
||
"moduleCode", "navigationCode", "moduleName", "alreadyOpen",
|
||
"traceSupported", "traceScope", "forceTerminationSupported",
|
||
"maxEvents", "maxDurationSeconds"
|
||
]);
|
||
const INITIALIZATION_TRACE_CAPTURE_POLICY =
|
||
"仅当前 ERP 托管 UI 线程;SQL 明细只覆盖 Lskj.Core.SqlHelper,另观察同线程旧日志中的数据库异常分类,不宣称覆盖模块直接 ADO.NET 的 SQL 文本;20 秒只限制 SQL 证据窗口,不会强制终止旧模块初始化;表/字段/过程/参数/调用位置使用会话内别名;不采集参数值或原始异常;不持久化原始 SQL";
|
||
const INITIALIZATION_TRACE_RISK_WARNING =
|
||
"20 秒仅限制 SQL 证据采集窗口,旧版 UI 初始化无法安全强制终止;若模块可能卡死,请先在隔离测试环境复现。";
|
||
const DYNAMIC_CREATE_PREVIEW_KEYS = Object.freeze([
|
||
"masterValues", "detailRows"
|
||
]);
|
||
const DYNAMIC_VALUE_KEYS = Object.freeze([
|
||
"parameterId", "label", "valueType", "value"
|
||
]);
|
||
const DYNAMIC_DETAIL_ROW_KEYS = Object.freeze(["rowNumber", "values"]);
|
||
const DYNAMIC_UPDATE_PREVIEW_KEYS = Object.freeze([
|
||
"recordDisplay", "changes"
|
||
]);
|
||
const DYNAMIC_UPDATE_CHANGE_KEYS = Object.freeze([
|
||
"parameterId", "label", "valueType", "previousValue", "newValue"
|
||
]);
|
||
const DYNAMIC_ADAPTER_KEYS = Object.freeze([
|
||
"id", "version", "evidenceSha256"
|
||
]);
|
||
const DYNAMIC_VALUE_TYPES = new Set([
|
||
"string", "number", "boolean", "date", "local-date-time",
|
||
"time", "year-month", "local-date-half-day"
|
||
]);
|
||
const MAX_DYNAMIC_MASTER_VALUES = 512;
|
||
const MAX_DYNAMIC_DETAIL_ROWS = 1000;
|
||
const MAX_DYNAMIC_VALUES_PER_ROW = 512;
|
||
const MAX_DYNAMIC_TOTAL_VALUES = 5000;
|
||
const MAX_DYNAMIC_CREATE_CHARACTERS = 128 * 1024;
|
||
const MAX_DYNAMIC_UPDATE_CHANGES = 512;
|
||
const MAX_DYNAMIC_UPDATE_CHARACTERS = 256 * 1024;
|
||
const MAX_DYNAMIC_VALUE_CHARACTERS = 32768;
|
||
|
||
function animation(row, durations) {
|
||
return Object.freeze({ row, frames: durations.length, durations: Object.freeze(durations.slice()) });
|
||
}
|
||
|
||
function requireAnimation(name) {
|
||
const value = ANIMATIONS[name];
|
||
if (!value) throw new Error("Unknown pet animation: " + name);
|
||
return value;
|
||
}
|
||
|
||
function animationForSemanticState(state) {
|
||
const name = SEMANTIC_STATES[state];
|
||
if (!name) throw new Error("Unknown pet state: " + state);
|
||
return name;
|
||
}
|
||
|
||
function frameStyle(animationName, frameIndex, assetUrl) {
|
||
const spec = requireAnimation(animationName);
|
||
if (!Number.isInteger(frameIndex) || frameIndex < 0 || frameIndex >= spec.frames) {
|
||
throw new RangeError("Frame index is outside animation row.");
|
||
}
|
||
return {
|
||
backgroundImage: assetUrl ? 'url("' + String(assetUrl).replace(/"/g, "%22") + '")' : "none",
|
||
backgroundSize: ATLAS.columns * 100 + "% " + ATLAS.rows * 100 + "%",
|
||
backgroundPosition: (frameIndex / (ATLAS.columns - 1)) * 100 + "% "
|
||
+ (spec.row / (ATLAS.rows - 1)) * 100 + "%"
|
||
};
|
||
}
|
||
|
||
function validateAtlas(width, height) {
|
||
return Number(width) === ATLAS.width && Number(height) === ATLAS.height;
|
||
}
|
||
|
||
function panelOpenAfterAnchorClick(panelOpen, openedByHover) {
|
||
if (typeof panelOpen !== "boolean" || typeof openedByHover !== "boolean") {
|
||
throw new TypeError("Panel interaction state must be boolean.");
|
||
}
|
||
return !panelOpen || openedByHover;
|
||
}
|
||
|
||
function quickActionPrompt(action) {
|
||
if (typeof action !== "string" || !Object.prototype.hasOwnProperty.call(
|
||
QUICK_ACTION_PROMPTS,
|
||
action)) {
|
||
throw new Error("Unknown pet quick action: " + String(action));
|
||
}
|
||
return QUICK_ACTION_PROMPTS[action];
|
||
}
|
||
|
||
function quickActionVisible(action, context) {
|
||
quickActionPrompt(action);
|
||
if (!ADMIN_QUICK_ACTIONS.has(action)) return true;
|
||
const scope = erpSessionScopeSummary(context);
|
||
return scope.complete && scope.isAdministrator;
|
||
}
|
||
|
||
function erpSessionScopeSummary(context) {
|
||
const userId = context && safeRequiredDisplayText(context.userId, 256);
|
||
const userName = context && safeRequiredDisplayText(context.userName, 500);
|
||
const accountBook = context
|
||
&& safeRequiredDisplayText(context.accountBook, 256);
|
||
const subSystemId = context
|
||
&& safeRequiredDisplayText(context.subSystemId, 256);
|
||
const subSystemName = context
|
||
&& safeRequiredDisplayText(context.subSystemName, 500);
|
||
const databaseScopeFingerprint = context
|
||
&& context.databaseScopeFingerprint;
|
||
if (!isRecord(context)
|
||
|| userId === null
|
||
|| userName === null
|
||
|| accountBook === null
|
||
|| subSystemId === null
|
||
|| subSystemName === null
|
||
|| typeof databaseScopeFingerprint !== "string"
|
||
|| !/^[a-f0-9]{64}$/.test(databaseScopeFingerprint)
|
||
|| typeof context.isAdministrator !== "boolean") {
|
||
return Object.freeze({
|
||
complete: false,
|
||
code: "erp_session_scope_invalid"
|
||
});
|
||
}
|
||
return Object.freeze({
|
||
complete: true,
|
||
code: null,
|
||
userId,
|
||
userName,
|
||
accountBook,
|
||
subSystemId,
|
||
subSystemName,
|
||
isAdministrator: context.isAdministrator,
|
||
databaseEvidence: databaseScopeFingerprint.slice(0, 12)
|
||
});
|
||
}
|
||
|
||
function loadAtlasAsset(assetUrl, options) {
|
||
const url = typeof assetUrl === "string" ? assetUrl.trim() : "";
|
||
if (!url || url.length > 2048 || /[\u0000-\u001f\u007f]/.test(url)) {
|
||
return Promise.reject(atlasError("pet_sprite_url_invalid"));
|
||
}
|
||
|
||
const settings = options || {};
|
||
const timeoutMs = settings.timeoutMs === undefined ? 10000 : Number(settings.timeoutMs);
|
||
if (!Number.isInteger(timeoutMs) || timeoutMs < 1000 || timeoutMs > 30000) {
|
||
return Promise.reject(atlasError("pet_sprite_timeout_invalid"));
|
||
}
|
||
const createImage = settings.createImage || function () {
|
||
if (typeof globalThis.Image !== "function") throw atlasError("pet_sprite_decoder_missing");
|
||
return new globalThis.Image();
|
||
};
|
||
const schedule = settings.setTimer || function (callback, delay) {
|
||
return globalThis.setTimeout(callback, delay);
|
||
};
|
||
const cancel = settings.clearTimer || function (timer) {
|
||
globalThis.clearTimeout(timer);
|
||
};
|
||
|
||
return new Promise((resolve, reject) => {
|
||
let image;
|
||
try {
|
||
image = createImage();
|
||
} catch (error) {
|
||
reject(normalizeAtlasError(error, "pet_sprite_decoder_missing"));
|
||
return;
|
||
}
|
||
if (!image || typeof image !== "object") {
|
||
reject(atlasError("pet_sprite_decoder_missing"));
|
||
return;
|
||
}
|
||
|
||
let settled = false;
|
||
let timer = null;
|
||
const finish = (error) => {
|
||
if (settled) return;
|
||
settled = true;
|
||
if (timer !== null) cancel(timer);
|
||
image.onload = null;
|
||
image.onerror = null;
|
||
if (error) reject(error);
|
||
else resolve(Object.freeze({
|
||
url,
|
||
width: Number(image.naturalWidth),
|
||
height: Number(image.naturalHeight)
|
||
}));
|
||
};
|
||
|
||
image.onload = async function () {
|
||
try {
|
||
if (typeof image.decode === "function") await image.decode();
|
||
if (!validateAtlas(image.naturalWidth, image.naturalHeight)) {
|
||
throw atlasError("pet_sprite_dimensions_invalid");
|
||
}
|
||
finish(null);
|
||
} catch (error) {
|
||
finish(normalizeAtlasError(error, "pet_sprite_decode_failed"));
|
||
}
|
||
};
|
||
image.onerror = function () {
|
||
finish(atlasError("pet_sprite_decode_failed"));
|
||
};
|
||
timer = schedule(function () {
|
||
finish(atlasError("pet_sprite_load_timeout"));
|
||
}, timeoutMs);
|
||
try {
|
||
image.decoding = "async";
|
||
image.src = url;
|
||
} catch (error) {
|
||
finish(normalizeAtlasError(error, "pet_sprite_decode_failed"));
|
||
}
|
||
});
|
||
}
|
||
|
||
function atlasError(code) {
|
||
const error = new Error("桌宠素材无法安全加载。");
|
||
error.code = code;
|
||
return error;
|
||
}
|
||
|
||
function normalizeAtlasError(error, fallbackCode) {
|
||
return error && typeof error === "object" && typeof error.code === "string"
|
||
? error
|
||
: atlasError(fallbackCode);
|
||
}
|
||
|
||
function purchaseLinePreview(plan) {
|
||
if (!plan || plan.commandName !== "purchase.invoice.create") {
|
||
return Object.freeze({ required: false, complete: true, code: null, lines: Object.freeze([]) });
|
||
}
|
||
const data = plan.data;
|
||
const header = plan.preview;
|
||
if (plan.outcomeCode !== "purchase_create_ready"
|
||
|| plan.title !== "采购发票创建预览"
|
||
|| !data || typeof data !== "object" || Array.isArray(data)
|
||
|| !exactFlatPreview(header, data.preview, PURCHASE_PREVIEW_KEYS)) {
|
||
return incompletePurchasePreview("purchase_header_preview_missing");
|
||
}
|
||
|
||
const supplier = displayText(header["供应商"], 256);
|
||
const invoiceNumber = displayText(header["发票号码"], 128);
|
||
const invoiceDate = dateOnlyValue(header["发票日期"]);
|
||
const currencyCode = displayText(header["币种"], 16);
|
||
const totalWithoutTax = finiteNumber(
|
||
header["不含税金额"], 0, 1000000000000000, true);
|
||
const taxAmount = finiteNumber(
|
||
header["税额"], 0, 1000000000000000, true);
|
||
const totalWithTax = finiteNumber(
|
||
header["价税合计"], 0, 2000000000000000, true);
|
||
const calculatedWithoutTax = finiteNumber(
|
||
header["明细汇总不含税"], 0, 1000000000000000, true);
|
||
const calculatedTax = finiteNumber(
|
||
header["明细汇总税额"], 0, 1000000000000000, true);
|
||
const calculatedWithTax = finiteNumber(
|
||
header["明细汇总价税"], 0, 2000000000000000, true);
|
||
const sourceAttachments = safeDisplayArray(header["来源附件"], 0, 3, 512);
|
||
if (supplier === null
|
||
|| invoiceNumber === null
|
||
|| invoiceDate === null
|
||
|| currencyCode === null
|
||
|| !safeBusinessCode(currencyCode, 16)
|
||
|| totalWithoutTax === null
|
||
|| taxAmount === null
|
||
|| totalWithTax === null
|
||
|| calculatedWithoutTax === null
|
||
|| calculatedTax === null
|
||
|| calculatedWithTax === null
|
||
|| sourceAttachments === null
|
||
|| !Number.isInteger(data.sourceDocumentCount)
|
||
|| data.sourceDocumentCount !== sourceAttachments.length
|
||
|| typeof data.sourceDocumentSetSha256 !== "string"
|
||
|| !/^[a-f0-9]{64}$/.test(data.sourceDocumentSetSha256)
|
||
|| header["重复发票"] !== false
|
||
|| !numbersEqual(totalWithTax, totalWithoutTax + taxAmount)) {
|
||
return incompletePurchasePreview("purchase_header_preview_invalid");
|
||
}
|
||
|
||
const source = data && data.lineMatches;
|
||
if (!Array.isArray(source)
|
||
|| source.length < 1
|
||
|| source.length > MAX_PURCHASE_PREVIEW_LINES) {
|
||
return incompletePurchasePreview("purchase_line_preview_missing");
|
||
}
|
||
|
||
const lines = [];
|
||
for (let index = 0; index < source.length; index += 1) {
|
||
const item = source[index];
|
||
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
||
return incompletePurchasePreview("purchase_line_preview_invalid");
|
||
}
|
||
const issues = item.issues;
|
||
const line = {
|
||
invoiceLineId: displayText(item.invoiceLineId, 128),
|
||
materialCode: displayText(item.materialCode, 256),
|
||
invoiceUnit: displayText(item.invoiceUnit, 64),
|
||
invoiceQuantity: finiteNumber(item.invoiceQuantity, 0, 1000000000, false),
|
||
invoiceUnitPrice: finiteNumber(item.invoiceUnitPrice, 0, 1000000000000, true),
|
||
invoiceTaxRate: finiteNumber(item.invoiceTaxRate, 0, 1, true),
|
||
invoiceTaxAmount: finiteNumber(item.invoiceTaxAmount, 0, 1000000000000000, true),
|
||
invoiceLineAmount: finiteNumber(item.invoiceLineAmount, 0, 1000000000000000, true),
|
||
sourceOrderNumber: displayText(item.sourceOrderNumber, 128),
|
||
sourceLineId: displayText(item.sourceLineId, 128),
|
||
sourceUnit: displayText(item.sourceUnit, 64),
|
||
sourceRemainingQuantity: finiteNumber(item.sourceRemainingQuantity, 0, 1000000000, false),
|
||
sourceUnitPrice: finiteNumber(item.sourceUnitPrice, 0, 1000000000000, true),
|
||
sourceTaxRate: finiteNumber(item.sourceTaxRate, 0, 1, true),
|
||
sourceExchangeRate: finiteNumber(item.sourceExchangeRate, 0, 1000000000, false),
|
||
status: item.status,
|
||
candidateCount: item.candidateCount,
|
||
issues
|
||
};
|
||
if (Object.values(line).some(value => value === null)
|
||
|| line.status !== "exact"
|
||
|| line.candidateCount !== 1
|
||
|| !Array.isArray(issues)
|
||
|| issues.length !== 0) {
|
||
return incompletePurchasePreview("purchase_line_preview_invalid");
|
||
}
|
||
lines.push(Object.freeze(line));
|
||
}
|
||
|
||
const lineWithoutTax = lines.reduce(
|
||
(total, line) => total + line.invoiceLineAmount, 0);
|
||
const lineTax = lines.reduce(
|
||
(total, line) => total + line.invoiceTaxAmount, 0);
|
||
const sourceOrders = distinctStringsIgnoreCase(
|
||
lines.map(line => line.sourceOrderNumber));
|
||
const sourceExchangeRates = distinctNumbers(
|
||
lines.map(line => line.sourceExchangeRate));
|
||
if (header["发票行数"] !== lines.length
|
||
|| header["确定匹配行数"] !== lines.length
|
||
|| !numbersEqual(calculatedWithoutTax, lineWithoutTax)
|
||
|| !numbersEqual(calculatedTax, lineTax)
|
||
|| !numbersEqual(calculatedWithTax, lineWithoutTax + lineTax)
|
||
|| !exactArray(header["来源采购单"], sourceOrders)
|
||
|| !exactArray(header["来源汇率"], sourceExchangeRates)) {
|
||
return incompletePurchasePreview("purchase_header_line_binding_invalid");
|
||
}
|
||
return Object.freeze({
|
||
required: true,
|
||
complete: true,
|
||
code: null,
|
||
header: Object.freeze({
|
||
supplier,
|
||
invoiceNumber,
|
||
invoiceDate: header["发票日期"],
|
||
currencyCode,
|
||
totalWithoutTax,
|
||
taxAmount,
|
||
totalWithTax,
|
||
sourceDocumentCount: sourceAttachments.length
|
||
}),
|
||
lines: Object.freeze(lines)
|
||
});
|
||
}
|
||
|
||
function incompletePurchasePreview(code) {
|
||
return Object.freeze({
|
||
required: true,
|
||
complete: false,
|
||
code,
|
||
lines: Object.freeze([])
|
||
});
|
||
}
|
||
|
||
function leaveConfirmationPreview(plan) {
|
||
const command = plan && plan.commandName;
|
||
if (command !== "hr.leave.create" && command !== "hr.leave.submit") {
|
||
return Object.freeze({ required: false, complete: true, code: null });
|
||
}
|
||
const source = plan && plan.preview;
|
||
const projected = plan && plan.data && plan.data.preview;
|
||
const expectedKeys = command === "hr.leave.create"
|
||
? LEAVE_CREATE_PREVIEW_KEYS
|
||
: LEAVE_SUBMIT_PREVIEW_KEYS;
|
||
if (!exactFlatPreview(source, projected, expectedKeys)) {
|
||
return incompleteLeavePreview("leave_confirmation_preview_missing");
|
||
}
|
||
|
||
if (command === "hr.leave.submit") {
|
||
const recordId = displayText(source["申请编号"], 128);
|
||
if (recordId === null || source["动作"] !== "提交审批") {
|
||
return incompleteLeavePreview("leave_submit_preview_invalid");
|
||
}
|
||
return Object.freeze({
|
||
required: true,
|
||
complete: true,
|
||
code: null,
|
||
recordId,
|
||
action: "提交审批"
|
||
});
|
||
}
|
||
|
||
const employeeId = displayText(source["员工"], 64);
|
||
const leaveTypeCode = displayText(source["请假类型"], 64);
|
||
const flowTypeCode = displayText(source["流转类别"], 64);
|
||
const reason = displayText(source["原因"], 500);
|
||
const startLocal = localDateTimeValue(source["开始时间"]);
|
||
const endLocal = localDateTimeValue(source["结束时间"]);
|
||
const calculatedHours = finiteNumber(source["核算工时"], 0, 744, false);
|
||
if (employeeId === null || !safeBusinessCode(employeeId, 64)
|
||
|| leaveTypeCode === null || !safeBusinessCode(leaveTypeCode, 64)
|
||
|| flowTypeCode === null || !safeBusinessCode(flowTypeCode, 64)
|
||
|| reason === null || reason.length < 2
|
||
|| startLocal === null || endLocal === null
|
||
|| startLocal >= endLocal
|
||
|| endLocal - startLocal > 31 * 24 * 60 * 60 * 1000
|
||
|| calculatedHours === null
|
||
|| source["创建后提交"] !== false) {
|
||
return incompleteLeavePreview("leave_create_preview_invalid");
|
||
}
|
||
return Object.freeze({
|
||
required: true,
|
||
complete: true,
|
||
code: null,
|
||
employeeId,
|
||
leaveTypeCode,
|
||
flowTypeCode,
|
||
startLocal: source["开始时间"],
|
||
endLocal: source["结束时间"],
|
||
calculatedHours,
|
||
reason,
|
||
submitAfterCreate: false
|
||
});
|
||
}
|
||
|
||
function incompleteLeavePreview(code) {
|
||
return Object.freeze({ required: true, complete: false, code });
|
||
}
|
||
|
||
function initializationTracePreview(plan) {
|
||
if (!plan || plan.commandName !== "module.trace-initialization") {
|
||
return Object.freeze({ required: false, complete: true, code: null });
|
||
}
|
||
const source = plan.preview;
|
||
const data = plan.data;
|
||
const projected = data && data.preview;
|
||
const warnings = plan.warnings;
|
||
if (plan.outcomeCode !== "plan_ready"
|
||
|| plan.title !== "复现并诊断模块初始化"
|
||
|| !exactFlatPreview(
|
||
source,
|
||
projected,
|
||
INITIALIZATION_TRACE_PREVIEW_KEYS)
|
||
|| source.moduleCode !== plan.moduleCode
|
||
|| !safeBusinessCode(source.moduleCode, 128)
|
||
|| displayText(source.navigationCode, 128) === null
|
||
|| displayText(source.moduleName, 256) === null
|
||
|| source.alreadyOpen !== false
|
||
|| source.traceSupported !== true
|
||
|| source.traceScope !== "current_erp_managed_ui_thread"
|
||
|| source.forceTerminationSupported !== false
|
||
|| source.maxEvents !== 200
|
||
|| source.maxDurationSeconds !== 20
|
||
|| !data || typeof data !== "object" || Array.isArray(data)
|
||
|| data.maxEvents !== 200
|
||
|| data.maxDurationSeconds !== 20
|
||
|| data.capturePolicy !== INITIALIZATION_TRACE_CAPTURE_POLICY
|
||
|| !data.staticDiagnosis
|
||
|| typeof data.staticDiagnosis !== "object"
|
||
|| Array.isArray(data.staticDiagnosis)
|
||
|| !Array.isArray(warnings)
|
||
|| !warnings.includes(INITIALIZATION_TRACE_RISK_WARNING)) {
|
||
return Object.freeze({
|
||
required: true,
|
||
complete: false,
|
||
code: "initialization_trace_preview_invalid"
|
||
});
|
||
}
|
||
return Object.freeze({
|
||
required: true,
|
||
complete: true,
|
||
code: null,
|
||
moduleCode: source.moduleCode,
|
||
navigationCode: source.navigationCode,
|
||
moduleName: source.moduleName,
|
||
maxEvents: 200,
|
||
maxDurationSeconds: 20,
|
||
forceTerminationSupported: false
|
||
});
|
||
}
|
||
|
||
function dynamicModuleConfirmationPreview(plan) {
|
||
const command = plan && plan.commandName;
|
||
if (command !== "module.record.create" && command !== "module.record.update") {
|
||
return Object.freeze({
|
||
required: false,
|
||
complete: true,
|
||
code: null,
|
||
mode: null
|
||
});
|
||
}
|
||
if (!plan || plan.commandVersion !== "1.0" || plan.risk !== "write") {
|
||
return incompleteDynamicModulePreview(
|
||
command,
|
||
"dynamic_module_command_contract_invalid");
|
||
}
|
||
return command === "module.record.create"
|
||
? dynamicModuleCreatePreview(plan)
|
||
: dynamicModuleUpdatePreview(plan);
|
||
}
|
||
|
||
function dynamicModuleCreatePreview(plan) {
|
||
const source = plan.preview;
|
||
const data = plan.data;
|
||
if (plan.outcomeCode !== "module_create_ready"
|
||
|| plan.title !== "低代码模块新增确认"
|
||
|| !isRecord(source)
|
||
|| !exactObjectKeys(source, DYNAMIC_CREATE_PREVIEW_KEYS)
|
||
|| !isRecord(data)
|
||
|| data.outcomeCode !== plan.outcomeCode
|
||
|| data.title !== plan.title
|
||
|| data.metadataTrust !== "untrusted_display_data"
|
||
|| data.genericWriteExecutionAvailable !== true
|
||
|| hasOwn(data, "writeExecutionBlocker")
|
||
|| typeof data.contractFingerprint !== "string"
|
||
|| !/^[a-f0-9]{64}$/.test(data.contractFingerprint)
|
||
|| !Array.isArray(data.issues)
|
||
|| data.issues.length !== 0
|
||
|| typeof data.lookupResolutionVerified !== "boolean"
|
||
|| !validDynamicAdapter(data.adapter)
|
||
|| !Array.isArray(source.masterValues)
|
||
|| source.masterValues.length > MAX_DYNAMIC_MASTER_VALUES
|
||
|| !Array.isArray(source.detailRows)
|
||
|| source.detailRows.length > MAX_DYNAMIC_DETAIL_ROWS) {
|
||
return incompleteDynamicModulePreview(
|
||
plan.commandName,
|
||
"dynamic_module_create_preview_invalid");
|
||
}
|
||
|
||
const master = normalizeDynamicValueArray(
|
||
source.masterValues,
|
||
"m",
|
||
MAX_DYNAMIC_MASTER_VALUES);
|
||
if (!master) {
|
||
return incompleteDynamicModulePreview(
|
||
plan.commandName,
|
||
"dynamic_module_create_values_invalid");
|
||
}
|
||
|
||
let totalValues = master.values.length;
|
||
let totalCharacters = master.characters;
|
||
const detailRows = [];
|
||
for (let index = 0; index < source.detailRows.length; index += 1) {
|
||
const row = source.detailRows[index];
|
||
if (!isRecord(row)
|
||
|| !exactObjectKeys(row, DYNAMIC_DETAIL_ROW_KEYS)
|
||
|| row.rowNumber !== index + 1
|
||
|| !Array.isArray(row.values)
|
||
|| row.values.length > MAX_DYNAMIC_VALUES_PER_ROW) {
|
||
return incompleteDynamicModulePreview(
|
||
plan.commandName,
|
||
"dynamic_module_create_detail_rows_invalid");
|
||
}
|
||
const normalized = normalizeDynamicValueArray(
|
||
row.values,
|
||
"d",
|
||
MAX_DYNAMIC_VALUES_PER_ROW);
|
||
if (!normalized) {
|
||
return incompleteDynamicModulePreview(
|
||
plan.commandName,
|
||
"dynamic_module_create_values_invalid");
|
||
}
|
||
totalValues += normalized.values.length;
|
||
totalCharacters += normalized.characters;
|
||
if (totalValues > MAX_DYNAMIC_TOTAL_VALUES
|
||
|| totalCharacters > MAX_DYNAMIC_CREATE_CHARACTERS) {
|
||
return incompleteDynamicModulePreview(
|
||
plan.commandName,
|
||
"dynamic_module_create_preview_too_large");
|
||
}
|
||
detailRows.push(Object.freeze({
|
||
rowNumber: row.rowNumber,
|
||
values: normalized.values
|
||
}));
|
||
}
|
||
|
||
if (totalValues > MAX_DYNAMIC_TOTAL_VALUES
|
||
|| totalCharacters > MAX_DYNAMIC_CREATE_CHARACTERS
|
||
|| !jsonEquivalentBounded(source, data.preview)
|
||
|| !jsonEquivalentBounded(source, data.parameterPreview)) {
|
||
return incompleteDynamicModulePreview(
|
||
plan.commandName,
|
||
"dynamic_module_create_projection_mismatch");
|
||
}
|
||
return Object.freeze({
|
||
required: true,
|
||
complete: true,
|
||
code: null,
|
||
mode: "create",
|
||
masterValues: master.values,
|
||
detailRows: Object.freeze(detailRows),
|
||
totalValues
|
||
});
|
||
}
|
||
|
||
function dynamicModuleUpdatePreview(plan) {
|
||
const source = plan.preview;
|
||
const data = plan.data;
|
||
if (plan.outcomeCode !== "dynamic_module_update_ready"
|
||
|| plan.title !== "基础档案并发修改确认"
|
||
|| !isRecord(source)
|
||
|| !exactObjectKeys(source, DYNAMIC_UPDATE_PREVIEW_KEYS)
|
||
|| safeRequiredDisplayText(source.recordDisplay, 256) === null
|
||
|| !Array.isArray(source.changes)
|
||
|| source.changes.length < 1
|
||
|| source.changes.length > MAX_DYNAMIC_UPDATE_CHANGES
|
||
|| !isRecord(data)
|
||
|| data.outcomeCode !== plan.outcomeCode
|
||
|| data.title !== plan.title
|
||
|| !validDynamicAdapter(data.adapter)) {
|
||
return incompleteDynamicModulePreview(
|
||
plan.commandName,
|
||
"dynamic_module_update_preview_invalid");
|
||
}
|
||
|
||
const parameterIds = new Set();
|
||
const changes = [];
|
||
let totalCharacters = 0;
|
||
for (const change of source.changes) {
|
||
if (!isRecord(change)
|
||
|| !exactObjectKeys(change, DYNAMIC_UPDATE_CHANGE_KEYS)
|
||
|| !validDynamicParameterId(change.parameterId, "m")
|
||
|| parameterIds.has(change.parameterId)
|
||
|| safeRequiredDisplayText(change.label, 80) === null
|
||
|| !DYNAMIC_VALUE_TYPES.has(change.valueType)
|
||
|| !safeDynamicValue(change.previousValue)
|
||
|| !safeDynamicValue(change.newValue)
|
||
|| change.previousValue === change.newValue) {
|
||
return incompleteDynamicModulePreview(
|
||
plan.commandName,
|
||
"dynamic_module_update_changes_invalid");
|
||
}
|
||
parameterIds.add(change.parameterId);
|
||
totalCharacters += change.previousValue.length + change.newValue.length;
|
||
if (totalCharacters > MAX_DYNAMIC_UPDATE_CHARACTERS) {
|
||
return incompleteDynamicModulePreview(
|
||
plan.commandName,
|
||
"dynamic_module_update_preview_too_large");
|
||
}
|
||
changes.push(Object.freeze({
|
||
parameterId: change.parameterId,
|
||
label: change.label,
|
||
valueType: change.valueType,
|
||
previousValue: change.previousValue,
|
||
newValue: change.newValue
|
||
}));
|
||
}
|
||
|
||
if (!jsonEquivalentBounded(source, data.preview)) {
|
||
return incompleteDynamicModulePreview(
|
||
plan.commandName,
|
||
"dynamic_module_update_projection_mismatch");
|
||
}
|
||
return Object.freeze({
|
||
required: true,
|
||
complete: true,
|
||
code: null,
|
||
mode: "update",
|
||
recordDisplay: source.recordDisplay,
|
||
changes: Object.freeze(changes)
|
||
});
|
||
}
|
||
|
||
function incompleteDynamicModulePreview(command, code) {
|
||
return Object.freeze({
|
||
required: true,
|
||
complete: false,
|
||
code,
|
||
mode: command === "module.record.update" ? "update" : "create"
|
||
});
|
||
}
|
||
|
||
function normalizeDynamicValueArray(values, requiredPrefix, maximumCount) {
|
||
if (!Array.isArray(values) || values.length > maximumCount) return null;
|
||
const parameterIds = new Set();
|
||
const normalized = [];
|
||
let characters = 0;
|
||
for (const item of values) {
|
||
if (!isRecord(item)
|
||
|| !exactObjectKeys(item, DYNAMIC_VALUE_KEYS)
|
||
|| !validDynamicParameterId(item.parameterId, requiredPrefix)
|
||
|| parameterIds.has(item.parameterId)
|
||
|| safeRequiredDisplayText(item.label, 80) === null
|
||
|| !DYNAMIC_VALUE_TYPES.has(item.valueType)
|
||
|| !safeDynamicValue(item.value)) return null;
|
||
parameterIds.add(item.parameterId);
|
||
characters += item.value.length;
|
||
normalized.push(Object.freeze({
|
||
parameterId: item.parameterId,
|
||
label: item.label,
|
||
valueType: item.valueType,
|
||
value: item.value
|
||
}));
|
||
}
|
||
return {
|
||
values: Object.freeze(normalized),
|
||
characters
|
||
};
|
||
}
|
||
|
||
function validDynamicAdapter(adapter) {
|
||
return isRecord(adapter)
|
||
&& exactObjectKeys(adapter, DYNAMIC_ADAPTER_KEYS)
|
||
&& safeRequiredCode(adapter.id, 128)
|
||
&& safeRequiredCode(adapter.version, 64)
|
||
&& typeof adapter.evidenceSha256 === "string"
|
||
&& /^[a-f0-9]{64}$/.test(adapter.evidenceSha256);
|
||
}
|
||
|
||
function validDynamicParameterId(value, requiredPrefix) {
|
||
return typeof value === "string"
|
||
&& value[0] === requiredPrefix
|
||
&& /^[md][0-9a-f]{16}$/.test(value);
|
||
}
|
||
|
||
function safeDynamicValue(value) {
|
||
return typeof value === "string"
|
||
&& value.length <= MAX_DYNAMIC_VALUE_CHARACTERS
|
||
&& !/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/.test(value);
|
||
}
|
||
|
||
function safeRequiredDisplayText(value, maximumLength) {
|
||
return typeof value === "string"
|
||
&& value.trim()
|
||
&& value.length <= maximumLength
|
||
&& !/[\u0000-\u001f\u007f-\u009f]/.test(value)
|
||
? value
|
||
: null;
|
||
}
|
||
|
||
function safeRequiredCode(value, maximumLength) {
|
||
const checked = safeRequiredDisplayText(value, maximumLength);
|
||
return checked !== null && safeBusinessCode(checked, maximumLength);
|
||
}
|
||
|
||
function isRecord(value) {
|
||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||
}
|
||
|
||
function hasOwn(value, name) {
|
||
return Object.prototype.hasOwnProperty.call(value, name);
|
||
}
|
||
|
||
function exactObjectKeys(value, expectedKeys) {
|
||
if (!isRecord(value)) return false;
|
||
const keys = Object.keys(value);
|
||
return keys.length === expectedKeys.length
|
||
&& keys.every(key => expectedKeys.includes(key))
|
||
&& expectedKeys.every(key => hasOwn(value, key));
|
||
}
|
||
|
||
function jsonEquivalentBounded(left, right) {
|
||
return jsonEquivalentNode(left, right, { remaining: 40000 }, 0);
|
||
}
|
||
|
||
function jsonEquivalentNode(left, right, budget, depth) {
|
||
budget.remaining -= 1;
|
||
if (budget.remaining < 0 || depth > 8) return false;
|
||
if (left === null || right === null) return left === right;
|
||
if (typeof left !== typeof right) return false;
|
||
if (typeof left !== "object") {
|
||
return typeof left !== "number"
|
||
? Object.is(left, right)
|
||
: Number.isFinite(left) && Number.isFinite(right) && Object.is(left, right);
|
||
}
|
||
if (Array.isArray(left) || Array.isArray(right)) {
|
||
return Array.isArray(left)
|
||
&& Array.isArray(right)
|
||
&& left.length === right.length
|
||
&& left.every((item, index) => jsonEquivalentNode(
|
||
item,
|
||
right[index],
|
||
budget,
|
||
depth + 1));
|
||
}
|
||
const leftKeys = Object.keys(left);
|
||
const rightKeys = Object.keys(right);
|
||
return leftKeys.length === rightKeys.length
|
||
&& leftKeys.every(key => hasOwn(right, key)
|
||
&& jsonEquivalentNode(left[key], right[key], budget, depth + 1));
|
||
}
|
||
|
||
function exactFlatPreview(source, projected, expectedKeys) {
|
||
if (!source || typeof source !== "object" || Array.isArray(source)
|
||
|| !projected || typeof projected !== "object" || Array.isArray(projected)) {
|
||
return false;
|
||
}
|
||
const sourceKeys = Object.keys(source);
|
||
const projectedKeys = Object.keys(projected);
|
||
if (sourceKeys.length !== expectedKeys.length
|
||
|| projectedKeys.length !== expectedKeys.length
|
||
|| expectedKeys.some(key => !Object.prototype.hasOwnProperty.call(source, key)
|
||
|| !Object.prototype.hasOwnProperty.call(projected, key)
|
||
|| !flatPreviewValueEqual(source[key], projected[key]))) return false;
|
||
return sourceKeys.every(key => expectedKeys.includes(key))
|
||
&& projectedKeys.every(key => expectedKeys.includes(key));
|
||
}
|
||
|
||
function flatPreviewValueEqual(left, right) {
|
||
if (Object.is(left, right)) return true;
|
||
if (!Array.isArray(left) || !Array.isArray(right)
|
||
|| left.length !== right.length) return false;
|
||
return left.every((value, index) => Object.is(value, right[index]));
|
||
}
|
||
|
||
function dateOnlyValue(value) {
|
||
if (typeof value !== "string") return null;
|
||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
||
if (!match) return null;
|
||
const [year, month, day] = match.slice(1).map(Number);
|
||
if (year < 1900 || year > 2100) return null;
|
||
const checked = new Date(Date.UTC(year, month - 1, day));
|
||
return checked.getUTCFullYear() === year
|
||
&& checked.getUTCMonth() === month - 1
|
||
&& checked.getUTCDate() === day
|
||
? checked.getTime()
|
||
: null;
|
||
}
|
||
|
||
function safeDisplayArray(value, minimumCount, maximumCount, maximumLength) {
|
||
if (!Array.isArray(value)
|
||
|| value.length < minimumCount
|
||
|| value.length > maximumCount) return null;
|
||
const result = value.map(item => displayText(item, maximumLength));
|
||
return result.some(item => item === null) ? null : result;
|
||
}
|
||
|
||
function distinctStringsIgnoreCase(values) {
|
||
const seen = new Set();
|
||
const result = [];
|
||
values.forEach(value => {
|
||
const key = value.toLocaleLowerCase("en-US");
|
||
if (!seen.has(key)) {
|
||
seen.add(key);
|
||
result.push(value);
|
||
}
|
||
});
|
||
return result;
|
||
}
|
||
|
||
function distinctNumbers(values) {
|
||
const result = [];
|
||
values.forEach(value => {
|
||
if (!result.some(existing => Object.is(existing, value))) result.push(value);
|
||
});
|
||
return result;
|
||
}
|
||
|
||
function exactArray(actual, expected) {
|
||
return Array.isArray(actual)
|
||
&& actual.length === expected.length
|
||
&& actual.every((value, index) => typeof value === "number"
|
||
&& typeof expected[index] === "number"
|
||
? numbersEqual(value, expected[index])
|
||
: Object.is(value, expected[index]));
|
||
}
|
||
|
||
function numbersEqual(left, right) {
|
||
return typeof left === "number"
|
||
&& typeof right === "number"
|
||
&& Number.isFinite(left)
|
||
&& Number.isFinite(right)
|
||
&& Math.abs(left - right) <= 0.000001;
|
||
}
|
||
|
||
function localDateTimeValue(value) {
|
||
if (typeof value !== "string") return null;
|
||
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})$/.exec(value);
|
||
if (!match) return null;
|
||
const parts = match.slice(1).map(Number);
|
||
const [year, month, day, hour, minute, second] = parts;
|
||
if (year < 1900 || year > 2100) return null;
|
||
const timestamp = Date.UTC(year, month - 1, day, hour, minute, second);
|
||
const checked = new Date(timestamp);
|
||
return checked.getUTCFullYear() === year
|
||
&& checked.getUTCMonth() === month - 1
|
||
&& checked.getUTCDate() === day
|
||
&& checked.getUTCHours() === hour
|
||
&& checked.getUTCMinutes() === minute
|
||
&& checked.getUTCSeconds() === second
|
||
? timestamp
|
||
: null;
|
||
}
|
||
|
||
function safeBusinessCode(value, maximumLength) {
|
||
if (typeof value !== "string") return false;
|
||
const normalized = value.trim();
|
||
if (!normalized || normalized.length > maximumLength) return false;
|
||
for (const character of normalized) {
|
||
if (!/[\p{L}\p{N}_.:-]/u.test(character)) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function displayText(value, maximumLength) {
|
||
if (typeof value !== "string") return null;
|
||
const normalized = value.trim();
|
||
if (!normalized || normalized.length > maximumLength
|
||
|| /[\u0000-\u001f\u007f]/.test(normalized)) return null;
|
||
return normalized;
|
||
}
|
||
|
||
function finiteNumber(value, minimum, maximum, allowMinimum) {
|
||
return typeof value === "number"
|
||
&& Number.isFinite(value)
|
||
&& (allowMinimum ? value >= minimum : value > minimum)
|
||
&& value <= maximum
|
||
? value
|
||
: null;
|
||
}
|
||
|
||
function previewReviewComplete(scrollTop, clientHeight, scrollHeight) {
|
||
if (![scrollTop, clientHeight, scrollHeight].every(value =>
|
||
typeof value === "number" && Number.isFinite(value) && value >= 0)
|
||
|| clientHeight <= 0
|
||
|| scrollHeight < clientHeight) return false;
|
||
return scrollTop + clientHeight >= scrollHeight - 2;
|
||
}
|
||
|
||
function diagnosticExecutionSummary(result) {
|
||
if (!isRecord(result) || !isRecord(result.data)) return null;
|
||
const data = result.data;
|
||
const code = typeof data.primaryFindingCode === "string"
|
||
&& /^[a-z0-9_.-]{1,128}$/.test(data.primaryFindingCode)
|
||
? data.primaryFindingCode
|
||
: null;
|
||
if (data.diagnosticContextSchemaVersion !== "1.0"
|
||
|| !code
|
||
|| !["failed", "degraded", "healthy"].includes(data.outcome)
|
||
|| typeof data.traceTruncated !== "boolean"
|
||
|| typeof data.summaryTruncated !== "boolean"
|
||
|| typeof data.evidencePersisted !== "boolean"
|
||
|| data.contextAvailable !== true) return null;
|
||
const labels = Object.freeze({
|
||
missing_object: "初始化引用的数据库对象不存在",
|
||
missing_column: "初始化引用的数据库字段不存在",
|
||
procedure_parameter: "初始化存储过程参数合同不匹配",
|
||
database_permission: "当前账套连接用户缺少数据库权限",
|
||
timeout: "初始化 SQL 执行超时",
|
||
connection: "初始化期间数据库连接异常",
|
||
conversion: "初始化期间发生数据类型转换失败",
|
||
constraint: "初始化期间发生数据约束冲突",
|
||
database_error: "初始化 SQL 执行失败",
|
||
slow_initialization_query: "初始化查询耗时过长",
|
||
module_initialization_error: "模块初始化失败,但没有捕获到可归因的 SQL 异常",
|
||
trace_truncated: "初始化追踪达到安全上限",
|
||
unclassified_module_error: "检测到模块错误,但现有证据不足以确定具体配置项",
|
||
no_failure_observed: "本次复现未捕获初始化故障"
|
||
});
|
||
const label = labels[code] || "初始化诊断已完成";
|
||
let text = "诊断结论:" + label + "(证据代码:" + code + ")。"
|
||
+ "这份脱敏证据已绑定到下一轮对话,10 分钟内可以继续问“具体哪里配置错了?”。";
|
||
if (data.traceTruncated || data.summaryTruncated) {
|
||
text += " 本次证据不完整,后续解释必须保留这一限制。";
|
||
}
|
||
if (!data.evidencePersisted) {
|
||
text += " 持久诊断证据未保存,请使用关联 ID 联系管理员核对。";
|
||
}
|
||
return text;
|
||
}
|
||
|
||
function bridgeFailureSummary(error) {
|
||
const response = error && isRecord(error.response) ? error.response : null;
|
||
const responseCode = response && safeFailureCode(response.code);
|
||
const localCode = error && safeFailureCode(error.code);
|
||
const code = responseCode || localCode || "bridge_error";
|
||
const trusted = response && trustedRecovery(response);
|
||
const recovery = trusted || localRecovery(code);
|
||
const message = trusted && safeFailureText(response.message, 2000)
|
||
? response.message
|
||
: localFailureMessage(code);
|
||
const correlationId = response
|
||
&& typeof response.correlationId === "string"
|
||
&& /^[A-Za-z0-9_.:-]{8,128}$/.test(response.correlationId)
|
||
? response.correlationId
|
||
: null;
|
||
const details = ["下一步:" + recovery.message, "错误码:" + code];
|
||
if (correlationId) details.push("关联 ID:" + correlationId);
|
||
return Object.freeze({
|
||
text: message + "(" + details.join(";") + ")",
|
||
code,
|
||
action: recovery.action,
|
||
retryable: recovery.retryable,
|
||
planInvalidated: recovery.planInvalidated,
|
||
correlationId
|
||
});
|
||
}
|
||
|
||
function trustedRecovery(response) {
|
||
if (!isRecord(response.data)
|
||
|| Object.keys(response.data).length !== 1
|
||
|| !isRecord(response.data.recovery)) return null;
|
||
const recovery = response.data.recovery;
|
||
const fields = Object.keys(recovery).sort();
|
||
if (fields.join("|") !== "action|message|planInvalidated|retryable"
|
||
|| !new Set([
|
||
"review_and_retry",
|
||
"reconcile_execution",
|
||
"restart_erp_pet",
|
||
"inspect_existing_record",
|
||
"replan",
|
||
"correct_request",
|
||
"contact_administrator",
|
||
"wait_and_retry",
|
||
"contact_support"
|
||
]).has(recovery.action)
|
||
|| typeof recovery.retryable !== "boolean"
|
||
|| typeof recovery.planInvalidated !== "boolean"
|
||
|| !safeFailureText(recovery.message, 300)) return null;
|
||
return Object.freeze({
|
||
action: recovery.action,
|
||
retryable: recovery.retryable,
|
||
planInvalidated: recovery.planInvalidated,
|
||
message: recovery.message
|
||
});
|
||
}
|
||
|
||
function localRecovery(code) {
|
||
if (code === "user_cancelled") return Object.freeze({
|
||
action: "review_and_retry",
|
||
retryable: true,
|
||
planInvalidated: false,
|
||
message: "本次没有写入;核对原预览后可再次确认执行。"
|
||
});
|
||
if (code === "bridge_timeout" || code === "workflow_database_error") {
|
||
return Object.freeze({
|
||
action: "reconcile_execution",
|
||
retryable: true,
|
||
planInvalidated: false,
|
||
message: "先检查 ERP 确认窗口和审计记录;无法确认结果时使用原预览重试,系统会复用幂等键。"
|
||
});
|
||
}
|
||
if (code === "bridge_closed"
|
||
|| code === "bridge_unavailable"
|
||
|| code === "erp_session_scope_invalid") {
|
||
return Object.freeze({
|
||
action: "restart_erp_pet",
|
||
retryable: false,
|
||
planInvalidated: true,
|
||
message: "确认目标 ERP 登录范围后,从该 ERP 重新启动桌宠并重新生成预览。"
|
||
});
|
||
}
|
||
return Object.freeze({
|
||
action: "contact_support",
|
||
retryable: false,
|
||
planInvalidated: true,
|
||
message: "不要重复执行旧计划;请使用关联 ID 查询审计记录后再重新发起。"
|
||
});
|
||
}
|
||
|
||
function localFailureMessage(code) {
|
||
if (code === "user_cancelled") return "用户已取消操作。";
|
||
if (code === "bridge_timeout") return "ERP 命令桥响应超时。";
|
||
if (code === "bridge_closed") return "ERP 命令桥已关闭。";
|
||
if (code === "bridge_unavailable") return "ERP 命令桥暂时不可用。";
|
||
if (code === "erp_session_scope_invalid") return "当前 ERP 会话范围不完整或格式无效。";
|
||
if (code === "bridge_protocol_error") return "ERP 命令桥返回了无效协议消息。";
|
||
return "ERP 操作未完成。";
|
||
}
|
||
|
||
function safeFailureCode(value) {
|
||
return typeof value === "string" && /^[A-Za-z0-9_.:-]{1,128}$/.test(value)
|
||
? value
|
||
: null;
|
||
}
|
||
|
||
function safeFailureText(value, maximumLength) {
|
||
return typeof value === "string"
|
||
&& value.length >= 1
|
||
&& value.length <= maximumLength
|
||
&& !/[\u0000-\u001f\u007f-\u009f]/.test(value);
|
||
}
|
||
|
||
function isExecutablePlan(plan) {
|
||
const executableRisks = new Set(["navigate", "write", "critical"]);
|
||
const generallyExecutable = Boolean(plan
|
||
&& plan.planId
|
||
&& plan.valid === true
|
||
&& plan.executionAllowed === true
|
||
&& executableRisks.has(plan.risk));
|
||
if (!generallyExecutable) return false;
|
||
const linePreview = purchaseLinePreview(plan);
|
||
const leavePreview = leaveConfirmationPreview(plan);
|
||
const tracePreview = initializationTracePreview(plan);
|
||
const dynamicPreview = dynamicModuleConfirmationPreview(plan);
|
||
return (!linePreview.required || linePreview.complete)
|
||
&& (!leavePreview.required || leavePreview.complete)
|
||
&& (!tracePreview.required || tracePreview.complete)
|
||
&& (!dynamicPreview.required || dynamicPreview.complete);
|
||
}
|
||
|
||
class PetStateMachine {
|
||
constructor(initialState) {
|
||
this.state = initialState || "offline";
|
||
animationForSemanticState(this.state);
|
||
this.listeners = new Set();
|
||
}
|
||
|
||
set(nextState, detail) {
|
||
animationForSemanticState(nextState);
|
||
if (nextState === this.state && detail === undefined) return this.state;
|
||
const previous = this.state;
|
||
this.state = nextState;
|
||
const event = Object.freeze({ previous, state: nextState, detail: detail || null });
|
||
this.listeners.forEach(listener => listener(event));
|
||
return nextState;
|
||
}
|
||
|
||
subscribe(listener) {
|
||
if (typeof listener !== "function") throw new TypeError("listener must be a function");
|
||
this.listeners.add(listener);
|
||
return () => this.listeners.delete(listener);
|
||
}
|
||
}
|
||
|
||
class AtlasAnimator {
|
||
constructor(element, options) {
|
||
if (!element || !element.style) throw new TypeError("A sprite element is required.");
|
||
const settings = options || {};
|
||
this.element = element;
|
||
this.assetUrl = settings.assetUrl || "";
|
||
this.setTimer = settings.setTimer || function (callback, delay) {
|
||
return globalThis.setTimeout(callback, delay);
|
||
};
|
||
this.clearTimer = settings.clearTimer || function (timer) {
|
||
return globalThis.clearTimeout(timer);
|
||
};
|
||
this.reducedMotion = Boolean(settings.reducedMotion);
|
||
this.timer = null;
|
||
this.animationName = "idle";
|
||
this.frameIndex = 0;
|
||
this.generation = 0;
|
||
}
|
||
|
||
setAsset(assetUrl) {
|
||
this.assetUrl = assetUrl || "";
|
||
this.render();
|
||
}
|
||
|
||
playSemantic(state) {
|
||
return this.play(animationForSemanticState(state));
|
||
}
|
||
|
||
play(animationName) {
|
||
requireAnimation(animationName);
|
||
this.stop();
|
||
this.animationName = animationName;
|
||
this.frameIndex = 0;
|
||
this.render();
|
||
if (!this.reducedMotion) this.schedule(this.generation);
|
||
return animationName;
|
||
}
|
||
|
||
stop() {
|
||
this.generation += 1;
|
||
if (this.timer !== null) this.clearTimer(this.timer);
|
||
this.timer = null;
|
||
}
|
||
|
||
schedule(generation) {
|
||
const spec = requireAnimation(this.animationName);
|
||
const delay = spec.durations[this.frameIndex];
|
||
this.timer = this.setTimer(() => {
|
||
if (generation !== this.generation) return;
|
||
this.frameIndex = (this.frameIndex + 1) % spec.frames;
|
||
this.render();
|
||
this.schedule(generation);
|
||
}, delay);
|
||
}
|
||
|
||
render() {
|
||
const style = frameStyle(this.animationName, this.frameIndex, this.assetUrl);
|
||
this.element.style.backgroundImage = style.backgroundImage;
|
||
this.element.style.backgroundSize = style.backgroundSize;
|
||
this.element.style.backgroundPosition = style.backgroundPosition;
|
||
this.element.dataset.animation = this.animationName;
|
||
this.element.dataset.frame = String(this.frameIndex);
|
||
}
|
||
}
|
||
|
||
return Object.freeze({
|
||
ATLAS,
|
||
ANIMATIONS,
|
||
SEMANTIC_STATES,
|
||
QUICK_ACTION_PROMPTS,
|
||
animationForSemanticState,
|
||
frameStyle,
|
||
validateAtlas,
|
||
panelOpenAfterAnchorClick,
|
||
quickActionPrompt,
|
||
quickActionVisible,
|
||
erpSessionScopeSummary,
|
||
purchaseLinePreview,
|
||
leaveConfirmationPreview,
|
||
initializationTracePreview,
|
||
dynamicModuleConfirmationPreview,
|
||
previewReviewComplete,
|
||
diagnosticExecutionSummary,
|
||
bridgeFailureSummary,
|
||
loadAtlasAsset,
|
||
isExecutablePlan,
|
||
PetStateMachine,
|
||
AtlasAnimator
|
||
});
|
||
});
|