(function (root, factory) { const api = factory(); if (typeof module === "object" && module.exports) module.exports = api; if (root) root.LskjBridgeClient = api; })(typeof globalThis !== "undefined" ? globalThis : this, function () { "use strict"; const PROTOCOL_VERSION = "1.0"; const SAFE_IDENTIFIER = /^[A-Za-z0-9_.:-]{8,128}$/; const SAFE_CODE = /^[A-Za-z0-9_.:-]{1,128}$/; const SAFE_COMMAND = /^[A-Za-z0-9_.:-]{1,128}$/; const SAFE_PLAN_ID = /^[A-Fa-f0-9]{32}$/; const RESPONSE_FIELDS = new Set([ "protocolVersion", "requestId", "correlationId", "success", "code", "message", "data" ]); const REQUIRED_RESPONSE_FIELDS = [ "protocolVersion", "requestId", "correlationId", "success", "code", "data" ]; const METHODS = new Set([ "health", "capabilities.list", "context.get", "command.plan", "command.execute" ]); const RECOVERY_ACTIONS = new Set([ "review_and_retry", "reconcile_execution", "restart_erp_pet", "inspect_existing_record", "replan", "correct_request", "contact_administrator", "wait_and_retry", "contact_support" ]); const EXECUTE_DATA_FIELDS = ["result", "followupPlan", "followupCode"]; const EXECUTE_RESULT_FIELDS = [ "success", "code", "message", "recordId", "replayed", "data" ]; const DIAGNOSTIC_DATA_FIELDS = [ "diagnosticContextSchemaVersion", "diagnosticId", "correlationId", "evidencePersisted", "evidenceContentHash", "outcome", "primaryFindingCode", "moduleOpenSucceeded", "eventCount", "failedEventCount", "slowEventCount", "traceTruncated", "summaryTruncated", "findings", "staticDiagnosis", "contextAvailable" ]; const DIAGNOSTIC_FINDING_FIELDS = [ "severity", "code", "category", "stage", "confidence", "message", "recommendation", "occurrenceCount", "sqlFingerprint", "caller" ]; const STATIC_DIAGNOSIS_FIELDS = [ "moduleCode", "moduleKind", "healthy", "issueCount", "issues" ]; const STATIC_ISSUE_FIELDS = ["severity", "code", "source"]; const SAFE_HASH = /^[a-f0-9]{64}$/; const SAFE_DIAGNOSTIC_ID = /^diag-[A-Fa-f0-9]{32}$/; const SAFE_CALLER = /^caller_(?:[0-9]{4}|overflow)$/; function createId() { if (globalThis.crypto && typeof globalThis.crypto.randomUUID === "function") { return globalThis.crypto.randomUUID().replace(/-/g, ""); } return "id" + Date.now().toString(36) + Math.random().toString(36).slice(2); } class WebViewTransport { constructor(webview, timeoutMs) { if (!webview || typeof webview.postMessage !== "function") { throw new TypeError("WebView2 transport is unavailable."); } this.webview = webview; // 写操作需要用户阅读预览并在 ERP 原生窗口再次确认,默认保留 3 分钟。 this.timeoutMs = timeoutMs || 180000; this.pending = new Map(); this.onMessage = this.onMessage.bind(this); this.webview.addEventListener("message", this.onMessage); } send(request) { return new Promise((resolve, reject) => { if (this.pending.has(request.requestId)) { reject(protocolError("ERP 桥请求 ID 重复。")); return; } const timer = setTimeout(() => { this.pending.delete(request.requestId); reject(clientError("bridge_timeout", "ERP 命令桥响应超时。")); }, this.timeoutMs); this.pending.set(request.requestId, { resolve, reject, timer }); this.webview.postMessage({ type: "lserp.bridge.request", request }); }); } onMessage(event) { const envelope = event && event.data; if (!envelope || envelope.type !== "lserp.bridge.response" || !envelope.response) return; const response = envelope.response; const waiter = this.pending.get(response.requestId); if (!waiter) return; clearTimeout(waiter.timer); this.pending.delete(response.requestId); waiter.resolve(response); } dispose() { this.webview.removeEventListener("message", this.onMessage); this.pending.forEach(waiter => { clearTimeout(waiter.timer); waiter.reject(clientError("bridge_closed", "ERP 命令桥已关闭。")); }); this.pending.clear(); } } class BridgeClient { constructor(transport, options) { if (!transport || typeof transport.send !== "function") { throw new TypeError("A bridge transport is required."); } const settings = options || {}; this.transport = transport; this.clientSessionId = settings.clientSessionId || createId(); if (typeof this.clientSessionId !== "string" || !SAFE_IDENTIFIER.test(this.clientSessionId)) { throw new TypeError("clientSessionId has an invalid format."); } this.idempotencyByPlan = new Map(); } async request(method, payload, correlationId) { validateRequest(method, payload); const trustedCorrelationId = correlationId === undefined || correlationId === null ? createId() : correlationId; if (typeof trustedCorrelationId !== "string" || !SAFE_IDENTIFIER.test(trustedCorrelationId)) { throw protocolError("ERP 桥关联 ID 格式无效。"); } const request = { protocolVersion: PROTOCOL_VERSION, requestId: createId(), correlationId: trustedCorrelationId, clientSessionId: this.clientSessionId, method, payload: payload === undefined || payload === null ? {} : payload }; const response = validateResponse(await this.transport.send(request), request); if (response.success !== true) { const error = new Error(response.message || "ERP 命令执行失败。"); error.code = response.code; error.response = response; throw error; } return response.data; } health() { return this.request("health"); } context() { return this.request("context.get"); } capabilities() { return this.request("capabilities.list"); } plan(command, input) { return this.request("command.plan", { command, input: input === undefined || input === null ? {} : input }); } async execute(planId, bridgeCorrelationId) { if (typeof planId !== "string" || !SAFE_PLAN_ID.test(planId) || typeof bridgeCorrelationId !== "string" || !SAFE_IDENTIFIER.test(bridgeCorrelationId)) { throw protocolError("ERP 执行请求格式无效。"); } let key = this.idempotencyByPlan.get(planId); if (!key) { key = createId(); this.idempotencyByPlan.set(planId, key); } const data = await this.request( "command.execute", { planId, idempotencyKey: key }, bridgeCorrelationId); validateExecuteData(data, bridgeCorrelationId); const projected = Object.assign({}, data, { bridgeCorrelationId }); if (isObject(data.followupPlan)) { projected.followupPlan = Object.assign( {}, data.followupPlan, { bridgeCorrelationId }); } return projected; } } function validateRequest(method, payload) { if (!METHODS.has(method)) throw protocolError("ERP 桥方法不受支持。"); const value = payload === undefined || payload === null ? {} : payload; if (!isObject(value)) throw protocolError("ERP 桥 payload 必须是对象。"); const fields = Object.keys(value); if (method === "health" || method === "capabilities.list" || method === "context.get") { if (fields.length !== 0) throw protocolError("该 ERP 桥方法不接受 payload 字段。"); return; } if (method === "command.plan") { if (!hasExactFields(fields, ["command", "input"]) || typeof value.command !== "string" || !SAFE_COMMAND.test(value.command) || !isObject(value.input)) { throw protocolError("ERP 计划请求格式无效。"); } return; } if (!hasExactFields(fields, ["planId", "idempotencyKey"]) || typeof value.planId !== "string" || !SAFE_PLAN_ID.test(value.planId) || typeof value.idempotencyKey !== "string" || !SAFE_IDENTIFIER.test(value.idempotencyKey)) { throw protocolError("ERP 执行请求格式无效。"); } } function validateResponse(response, request) { if (!isObject(response)) throw protocolError("ERP 桥响应必须是对象。"); const fields = Object.keys(response); if (fields.some(field => !RESPONSE_FIELDS.has(field)) || REQUIRED_RESPONSE_FIELDS.some(field => !fields.includes(field)) || response.protocolVersion !== PROTOCOL_VERSION || typeof response.requestId !== "string" || !SAFE_IDENTIFIER.test(response.requestId) || response.requestId !== request.requestId || typeof response.correlationId !== "string" || !SAFE_IDENTIFIER.test(response.correlationId) || response.correlationId !== request.correlationId || typeof response.success !== "boolean" || typeof response.code !== "string" || !SAFE_CODE.test(response.code) || !isObject(response.data) || (response.message !== undefined && response.message !== null && typeof response.message !== "string") || (typeof response.message === "string" && response.message.length > 2000)) { throw protocolError("ERP 桥返回了无效或不匹配的协议消息。"); } if (response.success === false) validateErrorRecovery(response); return response; } function validateErrorRecovery(response) { if (typeof response.message !== "string" || !safeDisplayText(response.message, 1, 2000) || !hasExactFields(Object.keys(response.data), ["recovery"]) || !isObject(response.data.recovery)) { throw protocolError("ERP 桥错误恢复契约无效。"); } const recovery = response.data.recovery; if (!hasExactFields( Object.keys(recovery), ["action", "retryable", "planInvalidated", "message"]) || typeof recovery.action !== "string" || !RECOVERY_ACTIONS.has(recovery.action) || typeof recovery.retryable !== "boolean" || typeof recovery.planInvalidated !== "boolean" || typeof recovery.message !== "string" || !safeDisplayText(recovery.message, 1, 300)) { throw protocolError("ERP 桥错误恢复契约无效。"); } } function validateExecuteData(data, expectedCorrelationId) { if (!isObject(data) || !hasExactFields(Object.keys(data), EXECUTE_DATA_FIELDS) || !isObject(data.result) || !hasExactFields(Object.keys(data.result), EXECUTE_RESULT_FIELDS)) { throw protocolError("ERP 执行成功回执契约无效。"); } const result = data.result; if (result.success !== true || typeof result.code !== "string" || !SAFE_CODE.test(result.code) || !safeDisplayText(result.message, 1, 300) || (result.recordId !== null && !safeDisplayText(result.recordId, 1, 256)) || typeof result.replayed !== "boolean" || !isObject(result.data) || (data.followupPlan !== null && !isObject(data.followupPlan)) || (data.followupCode !== null && (typeof data.followupCode !== "string" || !SAFE_CODE.test(data.followupCode)))) { throw protocolError("ERP 执行成功回执契约无效。"); } const resultDataFields = Object.keys(result.data); if (resultDataFields.length === 0) return; validateDiagnosticData(result.data, expectedCorrelationId); } function validateDiagnosticData(data, expectedCorrelationId) { if (!hasExactFields(Object.keys(data), DIAGNOSTIC_DATA_FIELDS) || data.diagnosticContextSchemaVersion !== "1.0" || typeof data.diagnosticId !== "string" || !SAFE_DIAGNOSTIC_ID.test(data.diagnosticId) || data.correlationId !== expectedCorrelationId || typeof data.evidencePersisted !== "boolean" || (data.evidenceContentHash !== null && (typeof data.evidenceContentHash !== "string" || !SAFE_HASH.test(data.evidenceContentHash))) || data.evidencePersisted !== (data.evidenceContentHash !== null) || !["failed", "degraded", "healthy"].includes(data.outcome) || typeof data.primaryFindingCode !== "string" || !SAFE_CODE.test(data.primaryFindingCode) || typeof data.moduleOpenSucceeded !== "boolean" || !boundedInteger(data.eventCount, 0, 200) || !boundedInteger(data.failedEventCount, 0, data.eventCount) || !boundedInteger(data.slowEventCount, 0, data.eventCount) || typeof data.traceTruncated !== "boolean" || typeof data.summaryTruncated !== "boolean" || data.contextAvailable !== true || !Array.isArray(data.findings) || data.findings.length < 1 || data.findings.length > 16 || !isObject(data.staticDiagnosis)) { throw protocolError("ERP 初始化诊断回执契约无效。"); } data.findings.forEach((finding, index) => { if (!isObject(finding) || !hasExactFields(Object.keys(finding), DIAGNOSTIC_FINDING_FIELDS) || !["error", "warning", "info"].includes(finding.severity) || typeof finding.code !== "string" || !SAFE_CODE.test(finding.code) || typeof finding.category !== "string" || !SAFE_CODE.test(finding.category) || typeof finding.stage !== "string" || !SAFE_CODE.test(finding.stage) || !["observed", "inferred"].includes(finding.confidence) || !safeDisplayText(finding.message, 1, 300) || !safeDisplayText(finding.recommendation, 1, 500) || !boundedInteger(finding.occurrenceCount, 1, 200) || (finding.sqlFingerprint !== null && (typeof finding.sqlFingerprint !== "string" || !SAFE_HASH.test(finding.sqlFingerprint))) || (finding.caller !== null && (typeof finding.caller !== "string" || !SAFE_CALLER.test(finding.caller)))) { throw protocolError("ERP 初始化诊断 finding 契约无效。"); } if (index === 0 && finding.code !== data.primaryFindingCode) { throw protocolError("ERP 初始化诊断主结论不一致。"); } }); const staticDiagnosis = data.staticDiagnosis; if (!hasExactFields( Object.keys(staticDiagnosis), STATIC_DIAGNOSIS_FIELDS) || typeof staticDiagnosis.moduleCode !== "string" || !SAFE_COMMAND.test(staticDiagnosis.moduleCode) || !["base", "bill"].includes(staticDiagnosis.moduleKind) || typeof staticDiagnosis.healthy !== "boolean" || !boundedInteger(staticDiagnosis.issueCount, 0, 200) || !Array.isArray(staticDiagnosis.issues) || staticDiagnosis.issues.length > 32 || staticDiagnosis.issues.length > staticDiagnosis.issueCount) { throw protocolError("ERP 静态诊断回执契约无效。"); } staticDiagnosis.issues.forEach(issue => { if (!isObject(issue) || !hasExactFields(Object.keys(issue), STATIC_ISSUE_FIELDS) || !["error", "warning", "info"].includes(issue.severity) || typeof issue.code !== "string" || !SAFE_CODE.test(issue.code) || typeof issue.source !== "string" || !SAFE_CODE.test(issue.source)) { throw protocolError("ERP 静态诊断 issue 契约无效。"); } }); } function boundedInteger(value, minimum, maximum) { return Number.isInteger(value) && value >= minimum && value <= maximum; } function safeDisplayText(value, minimumLength, maximumLength) { return typeof value === "string" && value.length >= minimumLength && value.length <= maximumLength && !/[\u0000-\u001f\u007f-\u009f]/.test(value); } function hasExactFields(actual, expected) { return actual.length === expected.length && expected.every(field => actual.includes(field)); } function isObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } function protocolError(message) { return clientError("bridge_protocol_error", message || "ERP 桥协议错误。"); } function clientError(code, message) { const error = new Error(message || "ERP 桥调用失败。"); error.code = code; return error; } return Object.freeze({ createId, WebViewTransport, BridgeClient, validateResponse }); });