feat: add ERP agent pet bridge and startup guide

This commit is contained in:
郎速科技
2026-08-14 14:28:28 +08:00
parent a803070819
commit 4c08f4c948
252 changed files with 134752 additions and 72 deletions
+898
View File
@@ -0,0 +1,898 @@
(function () {
"use strict";
const runtime = window.LskjPetRuntime;
const bridgeApi = window.LskjBridgeClient;
const shell = document.getElementById("pet-shell");
const anchor = document.getElementById("pet-anchor");
const panel = document.getElementById("assistant-panel");
const sprite = document.getElementById("pet-sprite");
const status = document.getElementById("pet-status");
const connection = document.getElementById("connection-state");
const closePetButton = document.getElementById("close-pet");
const moduleTitle = document.getElementById("module-title");
const contextScope = document.getElementById("context-scope");
const conversation = document.getElementById("conversation");
const composer = document.getElementById("composer");
const input = document.getElementById("message-input");
const attachButton = document.getElementById("attach-files");
const sendButton = document.getElementById("send-message");
const attachmentList = document.getElementById("attachment-list");
const planPreview = document.getElementById("plan-preview");
const planTitle = document.getElementById("plan-title");
const planDetail = document.getElementById("plan-detail");
const executeButton = document.getElementById("execute-plan");
const quickActionButtons = Array.from(
document.querySelectorAll("[data-quick-action]"));
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const previewParameters = new URLSearchParams(window.location.search);
const previewMode = previewParameters.get("preview") === "1";
if (previewMode) shell.dataset.preview = "enabled";
const state = new runtime.PetStateMachine("offline");
const animator = new runtime.AtlasAnimator(sprite, { reducedMotion });
let bridge = null;
let activePlan = null;
let activeAssistantMessage = null;
let attachments = [];
let uploading = false;
let chatBusy = false;
let assetReady = false;
let bridgeConnected = false;
let assetLoadGeneration = 0;
let contextRefreshGeneration = 0;
let hoverOpenTimer = null;
let panelOpenedByHover = false;
let planReviewRequired = false;
let planReviewSatisfied = true;
state.subscribe(event => {
animator.playSemantic(event.state);
const labels = {
idle: "可以问我",
listening: "我在听",
thinking: "正在理解",
planning: "正在生成预览",
awaiting_confirmation: "等待确认",
executing: "ERP 正在执行",
success: "操作完成",
error: "需要检查",
offline: "等待 ERP"
};
status.textContent = labels[event.state] || "处理中";
});
animator.playSemantic("offline");
anchor.addEventListener("mouseenter", scheduleHoverOpen);
anchor.addEventListener("mouseleave", cancelHoverOpen);
anchor.addEventListener("click", () => {
cancelHoverOpen();
const open = runtime.panelOpenAfterAnchorClick(
shell.dataset.panel === "open",
panelOpenedByHover);
panelOpenedByHover = false;
setPanel(open, true);
});
composer.addEventListener("submit", onSubmit);
input.addEventListener("keydown", event => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
composer.requestSubmit();
}
});
executeButton.addEventListener("click", executeActivePlan);
planDetail.addEventListener("scroll", updatePlanReviewFromScroll);
attachButton.addEventListener("click", pickAttachments);
closePetButton.addEventListener("click", closePet);
quickActionButtons.forEach(button => {
button.addEventListener("click", () => {
if (button.disabled) return;
let prompt;
try {
prompt = runtime.quickActionPrompt(button.dataset.quickAction);
} catch (_) {
return;
}
sendChat(prompt, button.textContent.trim());
});
});
if (!previewMode && window.chrome && window.chrome.webview) {
window.chrome.webview.addEventListener("message", onHostMessage);
bridge = new bridgeApi.BridgeClient(new bridgeApi.WebViewTransport(window.chrome.webview));
connection.textContent = "正在验证素材";
connection.dataset.state = "offline";
} else {
appendMessage("system", "当前是浏览器预览模式;Windows 宿主启动后会连接 ERP 命令桥和 AstrBot。 ");
configureLocalPreview();
}
function configureLocalPreview() {
const localPreviewOrigin = window.location.protocol === "file:"
|| window.location.hostname === "127.0.0.1"
|| window.location.hostname === "localhost";
if (!localPreviewOrigin || previewParameters.get("preview") !== "1") return;
const previewSprite = previewParameters.get("previewSprite");
if (previewSprite) animator.setAsset(previewSprite);
applyContext({
userId: "DEMO-USER",
userName: "演示用户",
accountBook: "演示账套",
subSystemId: "DEMO-SCM",
subSystemName: "采购管理",
databaseScopeFingerprint: "a".repeat(64),
isAdministrator: false,
activeModule: {
moduleCode: "CGDD",
navigationCode: "NAV-CGDD",
moduleName: "采购订单"
}
});
connection.textContent = "原型预览";
connection.dataset.state = "online";
state.set("awaiting_confirmation");
setPanel(true);
const lineMatches = [
previewPurchaseLine("L1", "MAT-1001", 12, 320, 0.13, "PO-20260811-018", "PO-L1", 30),
previewPurchaseLine("L2", "MAT-2048", 5, 680, 0.13, "PO-20260811-018", "PO-L2", 8),
previewPurchaseLine("L3", "MAT-3106", 18, 190, 0.06, "PO-20260809-006", "PO-L7", 40)
];
const preview = {
"供应商": "示例供应商",
"发票号码": "INV-20260811-018",
"发票日期": "2026-08-11",
"币种": "CNY",
"不含税金额": 10660,
"税额": 1146.4,
"价税合计": 11806.4,
"来源附件": ["invoice.pdf (0123456789ab…)"],
"明细汇总不含税": 10660,
"明细汇总税额": 1146.4,
"明细汇总价税": 11806.4,
"发票行数": 3,
"确定匹配行数": 3,
"来源采购单": ["PO-20260811-018", "PO-20260809-006"],
"来源汇率": [1],
"重复发票": false
};
showPlan({
planId: "0123456789abcdef0123456789abcdef",
commandName: "purchase.invoice.create",
moduleCode: "CGDD",
risk: "write",
valid: true,
executionAllowed: true,
title: "采购发票识别结果",
preview,
data: { preview, lineMatches },
warnings: []
});
}
function previewPurchaseLine(
invoiceLineId,
materialCode,
invoiceQuantity,
invoiceUnitPrice,
invoiceTaxRate,
sourceOrderNumber,
sourceLineId,
sourceRemainingQuantity) {
const invoiceLineAmount = invoiceQuantity * invoiceUnitPrice;
const invoiceTaxAmount = Number(
(invoiceLineAmount * invoiceTaxRate).toFixed(2));
return {
invoiceLineId,
materialCode,
invoiceUnit: "件",
invoiceQuantity,
invoiceUnitPrice,
invoiceTaxRate,
invoiceTaxAmount,
invoiceLineAmount,
status: "exact",
sourceOrderNumber,
sourceLineId,
unit: "件",
remainingQuantity: sourceRemainingQuantity,
unitPrice: invoiceUnitPrice,
taxRate: invoiceTaxRate,
exchangeRate: 1,
sourceUnit: "件",
sourceRemainingQuantity,
sourceUnitPrice: invoiceUnitPrice,
sourceTaxRate: invoiceTaxRate,
sourceExchangeRate: 1,
candidateCount: 1,
issues: []
};
}
function setPanel(open, focusInput) {
shell.dataset.panel = open ? "open" : "closed";
anchor.setAttribute("aria-expanded", String(open));
panel.setAttribute("aria-hidden", String(!open));
if (!open) panelOpenedByHover = false;
if (open) {
refreshActiveContext();
if (focusInput === true) setTimeout(() => input.focus(), 180);
}
}
function scheduleHoverOpen() {
if (shell.dataset.panel === "open" || hoverOpenTimer !== null) return;
hoverOpenTimer = setTimeout(() => {
hoverOpenTimer = null;
// 悬停只展开,不抢走 ERP 当前控件的键盘焦点;点击才聚焦输入框。
panelOpenedByHover = true;
setPanel(true, false);
}, 480);
}
function cancelHoverOpen() {
if (hoverOpenTimer === null) return;
clearTimeout(hoverOpenTimer);
hoverOpenTimer = null;
}
async function refreshActiveContext() {
if (previewMode) return true;
if (!bridge || !bridgeConnected) return false;
const generation = ++contextRefreshGeneration;
try {
const context = await bridge.context();
if (generation !== contextRefreshGeneration) return false;
return applyContext(context);
} catch (_) {
// 展开动作不能制造错误气泡;真正发送问题时 AstrBot 会再次实时取上下文,
// 桥不可用则沿既有错误通道给出关联 ID。
if (generation === contextRefreshGeneration) applyContext(null);
return false;
}
}
function applyContext(context) {
const scope = runtime.erpSessionScopeSummary(context);
quickActionButtons.forEach(button => {
button.hidden = !runtime.quickActionVisible(
button.dataset.quickAction,
context);
});
const active = context && context.activeModule;
moduleTitle.textContent = active && active.moduleName
? active.moduleName
: context && context.subSystemName
? context.subSystemName
: "ERP 会话未确认";
if (!scope.complete) {
contextScope.textContent = "会话范围未确认,执行前必须重新验证";
contextScope.title = "";
contextScope.dataset.state = "error";
return false;
}
const administrator = scope.isAdministrator ? " · 管理员" : "";
contextScope.textContent = `账套 ${scope.accountBook} · ${scope.subSystemName} · ${scope.userName}${administrator} · 库 ${scope.databaseEvidence}`;
contextScope.title = `用户 ${scope.userId};子系统 ${scope.subSystemId};数据库作用域证据 ${scope.databaseEvidence}`;
contextScope.dataset.state = "bound";
return true;
}
function closePet() {
if (!previewMode && window.chrome && window.chrome.webview) {
window.chrome.webview.postMessage({ type: "lserp.window.close" });
return;
}
setPanel(false);
}
async function connectBridge() {
if (!assetReady || !bridge) return;
try {
await bridge.health();
const context = await bridge.context();
if (!applyContext(context)) {
const scopeError = new Error("当前 ERP 会话范围不完整或格式无效。");
scopeError.code = "erp_session_scope_invalid";
throw scopeError;
}
connection.textContent = "已连接";
connection.dataset.state = "online";
bridgeConnected = true;
state.set("idle");
updateComposerState();
} catch (error) {
bridgeConnected = false;
applyContext(null);
connection.textContent = "连接失败";
connection.dataset.state = "error";
state.set("offline");
appendMessage("error", safeError(error));
updateComposerState();
}
}
async function configureSprite(assetUrl) {
const generation = ++assetLoadGeneration;
assetReady = false;
bridgeConnected = false;
animator.setAsset("");
connection.textContent = "正在验证素材";
connection.dataset.state = "offline";
updateComposerState();
try {
const loaded = await runtime.loadAtlasAsset(assetUrl);
if (generation !== assetLoadGeneration) return;
animator.setAsset(loaded.url);
assetReady = true;
await connectBridge();
} catch (error) {
if (generation !== assetLoadGeneration) return;
connection.textContent = "素材无效";
connection.dataset.state = "error";
state.set("error");
appendMessage("error", "guga 图集无法完整解码或尺寸不符合 1536×1872,桌宠已停止连接 ERP。");
updateComposerState();
}
}
function onHostMessage(event) {
const message = event && event.data;
if (!message) return;
if (message.type === "lserp.pet.configure" && message.spriteUrl) {
configureSprite(message.spriteUrl);
return;
}
if (message.type === "lserp.chat.delta") {
appendAssistantDelta(message.text || "");
return;
}
if (message.type === "lserp.chat.result") {
chatBusy = false;
updateComposerState();
state.set("idle");
const streamed = finishAssistantStream();
if (message.text && !streamed) appendMessage("assistant", message.text);
if (message.commandPlan) {
const correlatedPlan = Object.assign({}, message.commandPlan);
if (typeof message.bridgeCorrelationId === "string"
&& /^[A-Za-z0-9_.:-]{8,128}$/.test(message.bridgeCorrelationId)) {
correlatedPlan.bridgeCorrelationId = message.bridgeCorrelationId;
}
showPlan(correlatedPlan);
}
return;
}
if (message.type === "lserp.chat.error") {
chatBusy = false;
updateComposerState();
finishAssistantStream();
state.set("error");
appendMessage("error", appendCorrelation(
message.message || "对话服务暂时不可用。",
message.correlationId));
return;
}
if (message.type === "lserp.attachments.uploading") {
uploading = message.uploading === true;
updateComposerState();
if (uploading) state.set("thinking");
else if (!chatBusy && state.state === "thinking") state.set("idle");
return;
}
if (message.type === "lserp.attachments.changed") {
attachments = Array.isArray(message.attachments)
? message.attachments.filter(item => item && typeof item.attachmentId === "string")
: [];
renderAttachments();
updateComposerState();
return;
}
if (message.type === "lserp.attachments.error") {
appendMessage("error", appendCorrelation(
message.message || "附件处理失败。",
message.correlationId));
state.set("error");
}
}
function onSubmit(event) {
event.preventDefault();
const text = input.value.trim();
if ((!text && attachments.length === 0)
|| uploading || chatBusy || !bridgeConnected) return;
sendChat(
text,
text || `已发送 ${attachments.length} 个附件,请识别并生成业务预览。`);
}
function sendChat(text, visibleText) {
if ((!text && attachments.length === 0)
|| uploading || chatBusy || !bridgeConnected) return false;
appendMessage("user", visibleText || text);
finishAssistantStream();
activePlan = null;
planReviewRequired = false;
planReviewSatisfied = true;
planPreview.hidden = true;
executeButton.disabled = true;
input.value = "";
resizeInput();
chatBusy = true;
updateComposerState();
state.set("thinking");
if (window.chrome && window.chrome.webview) {
window.chrome.webview.postMessage({ type: "lserp.chat.send", text });
} else {
setTimeout(() => {
chatBusy = false;
updateComposerState();
state.set("idle");
appendMessage("assistant", "浏览器预览没有连接 AstrBot。正式宿主会把这条消息连同当前 ERP 上下文发送给机器人。");
}, 320);
}
return true;
}
function pickAttachments() {
if (uploading || chatBusy || !bridgeConnected) return;
if (window.chrome && window.chrome.webview) {
window.chrome.webview.postMessage({ type: "lserp.attachment.pick" });
return;
}
appendMessage("system", "浏览器预览不能读取本地附件;Windows 宿主会打开原生选件窗口。");
}
function renderAttachments() {
attachmentList.replaceChildren();
attachmentList.hidden = attachments.length === 0;
attachments.forEach(item => {
const chip = document.createElement("span");
chip.className = "attachment-chip";
const name = document.createElement("span");
name.className = "attachment-name";
name.textContent = item.fileName || "附件";
name.title = item.fileName || "附件";
const remove = document.createElement("button");
remove.type = "button";
remove.className = "attachment-remove";
remove.setAttribute("aria-label", "移除附件 " + (item.fileName || ""));
remove.textContent = "×";
remove.disabled = uploading || chatBusy;
remove.addEventListener("click", () => {
if (!window.chrome || !window.chrome.webview) return;
window.chrome.webview.postMessage({
type: "lserp.attachment.remove",
attachmentId: item.attachmentId
});
});
chip.append(name, remove);
attachmentList.appendChild(chip);
});
}
function updateComposerState() {
const unavailable = !previewMode && !bridgeConnected;
attachButton.disabled = uploading || chatBusy || unavailable;
input.disabled = uploading || chatBusy || unavailable;
sendButton.disabled = uploading || chatBusy || unavailable
|| (!input.value.trim() && attachments.length === 0);
attachButton.textContent = uploading ? "上传中" : "+附件";
attachmentList.querySelectorAll(".attachment-remove").forEach(button => {
button.disabled = uploading || chatBusy || unavailable;
});
quickActionButtons.forEach(button => {
// 快捷问题永不隐式携带待处理发票;用户必须先发送或移除附件。
button.disabled = uploading || chatBusy || unavailable
|| attachments.length > 0;
});
}
function showPlan(plan) {
if (!plan || !plan.planId) return;
const purchasePreview = runtime.purchaseLinePreview(plan);
const leavePreview = runtime.leaveConfirmationPreview(plan);
const tracePreview = runtime.initializationTracePreview(plan);
const dynamicPreview = runtime.dynamicModuleConfirmationPreview(plan);
const correlationBound = plan
&& typeof plan.bridgeCorrelationId === "string"
&& /^[A-Za-z0-9_.:-]{8,128}$/.test(plan.bridgeCorrelationId);
const executable = runtime.isExecutablePlan(plan)
&& (previewMode || correlationBound);
activePlan = executable ? plan : null;
planReviewRequired = executable
&& ((purchasePreview.required && purchasePreview.complete)
|| (dynamicPreview.required && dynamicPreview.complete));
planReviewSatisfied = !planReviewRequired;
planTitle.textContent = plan.title || plan.commandName || "业务操作预览";
const preview = dynamicPreview.required
? {
"命令": plan.commandName,
"模块": plan.moduleCode,
"风险": plan.risk,
"计划号": plan.planId,
"动态配置": dynamicPreview.complete
? dynamicPreview.mode === "create"
? `${dynamicPreview.totalValues} 个参数,${dynamicPreview.detailRows.length} 行明细`
: `${dynamicPreview.changes.length} 个字段将修改`
: "确认结构未通过安全校验"
}
: Object.assign({}, plan.preview || (plan.data && plan.data.preview) || {
"命令": plan.commandName,
"模块": plan.moduleCode,
"风险": plan.risk,
"计划号": plan.planId
});
if (Array.isArray(plan.warnings) && plan.warnings.length) {
preview["需处理"] = plan.warnings.slice(0, 5).join("");
}
if (purchasePreview.required && !purchasePreview.complete) {
preview["安全检查"] = "逐行匹配证据不完整,已禁止执行;请重新生成采购预览。";
}
if (leavePreview.required && !leavePreview.complete) {
preview["安全检查"] = "请假确认信息不完整,已禁止执行;请重新生成请假预览。";
}
if (tracePreview.required && !tracePreview.complete) {
preview["安全检查"] = "初始化诊断的模块范围或风险说明不完整,已禁止执行。";
}
if (dynamicPreview.required && !dynamicPreview.complete) {
preview["安全检查"] = "低代码动态参数、并发快照或适配器证据不完整,已禁止执行。";
}
const detailNodes = [buildDefinitionList(preview, 24)];
if (purchasePreview.complete && purchasePreview.lines.length) {
detailNodes.push(buildPurchaseLineList(purchasePreview.lines));
}
if (dynamicPreview.required && dynamicPreview.complete) {
detailNodes.push(buildDynamicModulePreview(dynamicPreview));
}
planDetail.replaceChildren(...detailNodes);
planDetail.scrollTop = 0;
planPreview.hidden = false;
executeButton.disabled = !executable
|| (!previewMode && !bridgeConnected)
|| !planReviewSatisfied;
executeButton.textContent = executable && !planReviewSatisfied
? "请先向下滚动核对全部内容"
: executable
? "在 ERP 中确认并执行"
: purchasePreview.required && !purchasePreview.complete
? "逐行匹配证据不完整,不能执行"
: leavePreview.required && !leavePreview.complete
? "请假确认信息不完整,不能执行"
: tracePreview.required && !tracePreview.complete
? "诊断范围或风险说明不完整,不能执行"
: dynamicPreview.required && !dynamicPreview.complete
? "动态参数或并发证据不完整,不能执行"
: plan.valid === false
? "预览未通过,不能执行"
: plan.risk === "read"
? "只读结果,无需执行"
: "解析预览,无需执行";
state.set(executable ? "awaiting_confirmation" : "idle");
if (planReviewRequired) {
requestAnimationFrame(updatePlanReviewFromScroll);
}
if (!previewMode) void refreshActiveContext();
}
function updatePlanReviewFromScroll() {
if (!planReviewRequired || planReviewSatisfied || !activePlan) return;
planReviewSatisfied = runtime.previewReviewComplete(
planDetail.scrollTop,
planDetail.clientHeight,
planDetail.scrollHeight);
if (planReviewSatisfied) syncActivePlanButton();
}
function syncActivePlanButton() {
if (!activePlan) {
executeButton.disabled = true;
return;
}
executeButton.disabled = (!previewMode && !bridgeConnected)
|| !planReviewSatisfied;
executeButton.textContent = planReviewSatisfied
? "在 ERP 中确认并执行"
: "请先向下滚动核对全部内容";
}
async function executeActivePlan() {
if (!activePlan || !bridge || !bridgeConnected || !planReviewSatisfied) return;
executeButton.disabled = true;
if (!previewMode && !await refreshActiveContext()) {
activePlan = null;
planReviewRequired = false;
planReviewSatisfied = false;
executeButton.disabled = true;
executeButton.textContent = "ERP 会话范围已变化,请重新生成预览";
appendMessage(
"error",
"当前 ERP 数据库、用户、账套或子系统范围无法重新确认;旧计划已作废,请确认登录范围后重新发起。"
);
state.set("error");
return;
}
state.set("executing");
try {
const data = await bridge.execute(
activePlan.planId,
activePlan.bridgeCorrelationId);
const result = data.result || {};
const receipt = result.recordId
? (result.message || "ERP 操作已完成。") + "(业务记录:" + result.recordId + ""
: result.message || "ERP 操作已完成。";
appendMessage("assistant", appendCorrelation(
receipt,
data.bridgeCorrelationId));
const diagnosticSummary = runtime.diagnosticExecutionSummary(result);
if (diagnosticSummary) appendMessage("system", diagnosticSummary);
executeButton.textContent = "在 ERP 中确认并执行";
if (data.followupPlan) {
showPlan(data.followupPlan);
appendMessage("system", "下一步仍是独立操作,请核对新预览后再次确认。");
} else {
if (data.followupCode) {
appendMessage(
"system",
"ERP 操作已完成,但后续操作预览未生成(" + data.followupCode + ")。后续操作尚未执行,请重新询问桌宠。"
);
}
planPreview.hidden = true;
activePlan = null;
planReviewRequired = false;
planReviewSatisfied = true;
state.set("success");
setTimeout(() => state.set("idle"), 1600);
}
} catch (error) {
const failure = runtime.bridgeFailureSummary(error);
appendMessage(
failure.code === "user_cancelled" ? "system" : "error",
failure.text);
if (failure.planInvalidated) {
activePlan = null;
planReviewRequired = false;
planReviewSatisfied = false;
planPreview.hidden = true;
executeButton.textContent = "请重新生成预览";
}
state.set(failure.code === "user_cancelled" ? "idle" : "error");
} finally {
syncActivePlanButton();
}
}
function appendMessage(kind, text) {
if (!text) return;
const paragraph = document.createElement("p");
paragraph.className = kind + "-message";
paragraph.textContent = text;
conversation.appendChild(paragraph);
conversation.scrollTop = conversation.scrollHeight;
}
function appendAssistantDelta(text) {
if (!text) return;
if (!activeAssistantMessage) {
activeAssistantMessage = document.createElement("p");
activeAssistantMessage.className = "assistant-message";
conversation.appendChild(activeAssistantMessage);
}
activeAssistantMessage.appendChild(document.createTextNode(text));
conversation.scrollTop = conversation.scrollHeight;
}
function finishAssistantStream() {
const hadStream = !!activeAssistantMessage;
activeAssistantMessage = null;
return hadStream;
}
function buildDefinitionList(values, maximumEntries) {
const list = document.createElement("dl");
const entries = Object.entries(values || {});
const limit = Number.isInteger(maximumEntries) ? maximumEntries : 24;
entries.slice(0, limit).forEach(([key, value]) => {
const term = document.createElement("dt");
const description = document.createElement("dd");
term.textContent = key;
description.textContent = value === null || value === undefined
? "-"
: typeof value === "object" ? JSON.stringify(value) : String(value);
list.append(term, description);
});
if (entries.length > limit) {
const term = document.createElement("dt");
const description = document.createElement("dd");
term.textContent = "更多字段";
description.textContent = `还有 ${entries.length - limit} 项,请重新生成更小的操作预览。`;
list.append(term, description);
}
return list;
}
function buildPurchaseLineList(lines) {
const section = document.createElement("section");
section.className = "purchase-line-preview";
const heading = document.createElement("h3");
heading.textContent = `逐行来源匹配(${lines.length} 行)`;
section.appendChild(heading);
lines.forEach((line, index) => {
const card = document.createElement("article");
card.className = "purchase-line-card";
const title = document.createElement("h4");
title.textContent = `${line.invoiceLineId || `${index + 1}`} · ${line.materialCode}`;
const status = document.createElement("span");
status.className = "purchase-match-status";
status.textContent = "唯一匹配";
const header = document.createElement("div");
header.className = "purchase-line-header";
header.append(title, status);
card.appendChild(header);
card.appendChild(buildDefinitionList({
"本次数量": `${displayNumber(line.invoiceQuantity)} ${line.invoiceUnit}`,
"本次单价": displayNumber(line.invoiceUnitPrice),
"本次税率": displayPercent(line.invoiceTaxRate),
"本次税额": displayNumber(line.invoiceTaxAmount),
"本次行金额": displayNumber(line.invoiceLineAmount),
"来源采购单": line.sourceOrderNumber,
"来源明细": line.sourceLineId,
"来源剩余": `${displayNumber(line.sourceRemainingQuantity)} ${line.sourceUnit}`,
"来源单价": displayNumber(line.sourceUnitPrice),
"来源税率": displayPercent(line.sourceTaxRate),
"来源汇率": displayNumber(line.sourceExchangeRate)
}, 16));
section.appendChild(card);
});
return section;
}
function buildDynamicModulePreview(preview) {
const section = document.createElement("section");
section.className = "dynamic-module-preview";
const heading = document.createElement("h3");
heading.textContent = preview.mode === "create"
? "低代码动态参数 · 新增"
: "低代码动态参数 · 并发修改";
section.appendChild(heading);
if (preview.mode === "update") {
section.appendChild(buildDefinitionList({
"目标记录": preview.recordDisplay,
"修改字段": preview.changes.length
}, 4));
const changes = document.createElement("div");
changes.className = "dynamic-change-list";
preview.changes.forEach(change => {
const card = dynamicFieldCard(
change.label,
change.parameterId,
change.valueType);
const comparison = document.createElement("div");
comparison.className = "dynamic-change-values";
comparison.append(
dynamicChangeValue("修改前", change.previousValue),
dynamicChangeValue("修改后", change.newValue));
card.appendChild(comparison);
changes.appendChild(card);
});
section.appendChild(changes);
return section;
}
const master = document.createElement("section");
master.className = "dynamic-value-group";
const masterHeading = document.createElement("h4");
masterHeading.textContent = `主表字段(${preview.masterValues.length} 项)`;
master.append(masterHeading, buildDynamicValueCards(preview.masterValues));
section.appendChild(master);
const details = document.createElement("section");
details.className = "dynamic-detail-preview";
const detailHeading = document.createElement("h4");
detailHeading.textContent = `明细数据(${preview.detailRows.length} 行)`;
details.appendChild(detailHeading);
if (preview.detailRows.length === 0) {
const empty = document.createElement("p");
empty.className = "dynamic-empty-value";
empty.textContent = "本次新增没有明细行。";
details.appendChild(empty);
} else {
preview.detailRows.forEach(row => {
const card = document.createElement("article");
card.className = "dynamic-detail-row";
const title = document.createElement("h5");
title.textContent = `${row.rowNumber} 行 · ${row.values.length} 个字段`;
card.append(title, buildDynamicValueCards(row.values));
details.appendChild(card);
});
}
section.appendChild(details);
return section;
}
function buildDynamicValueCards(values) {
const list = document.createElement("div");
list.className = "dynamic-value-list";
if (values.length === 0) {
const empty = document.createElement("p");
empty.className = "dynamic-empty-value";
empty.textContent = "没有需要写入的字段。";
list.appendChild(empty);
return list;
}
values.forEach(value => {
const card = dynamicFieldCard(
value.label,
value.parameterId,
value.valueType);
const content = document.createElement("div");
content.className = "dynamic-field-value";
content.textContent = value.value === "" ? "(空值)" : value.value;
card.appendChild(content);
list.appendChild(card);
});
return list;
}
function dynamicFieldCard(label, parameterId, valueType) {
const card = document.createElement("article");
card.className = "dynamic-field-card";
const header = document.createElement("div");
header.className = "dynamic-field-header";
const title = document.createElement("h5");
title.textContent = label;
const type = document.createElement("span");
type.className = "dynamic-value-type";
type.textContent = valueType;
header.append(title, type);
const identifier = document.createElement("code");
identifier.className = "dynamic-parameter-id";
identifier.textContent = parameterId;
card.append(header, identifier);
return card;
}
function dynamicChangeValue(label, value) {
const item = document.createElement("div");
const heading = document.createElement("span");
heading.textContent = label;
const content = document.createElement("div");
content.textContent = value === "" ? "(空值)" : value;
item.append(heading, content);
return item;
}
function displayNumber(value) {
return new Intl.NumberFormat("zh-CN", {
maximumFractionDigits: 8,
useGrouping: true
}).format(value);
}
function displayPercent(value) {
return new Intl.NumberFormat("zh-CN", {
style: "percent",
maximumFractionDigits: 6
}).format(value);
}
function safeError(error) {
return runtime.bridgeFailureSummary(error).text;
}
function appendCorrelation(message, correlationId) {
return correlationId && /^[A-Za-z0-9_.:-]{8,128}$/.test(correlationId)
? message + "(关联 ID" + correlationId + ""
: message;
}
function resizeInput() {
input.style.height = "auto";
input.style.height = Math.min(input.scrollHeight, 90) + "px";
}
input.addEventListener("input", () => {
resizeInput();
updateComposerState();
});
updateComposerState();
})();