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

409 lines
16 KiB
JavaScript

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