Files
lserp_cs_6.0/插件库/Lskj.AgentPet/pet-runtime.js
T
2026-08-14 14:28:28 +08:00

1271 lines
47 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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
});
});