(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 }); });