(function () {
"use strict";
const API_ROOT = "/api/self-repair";
const VIEW_KEY = "cognitive-agent.self-repair-view";
const MODE_KEY = "cognitive-agent.self-repair-execution";
const STATE_TEXT = {
OK: "正常",
WARN: "告警",
FAULT: "故障",
UNKNOWN: "未检测",
UNMONITORED: "关联待测",
NOT_USED: "未启用",
IDLE: "检测中",
};
const EVENT_META = {
BASELINE: { kind: "system", label: "运行" },
INJECTED: { kind: "fault", label: "注入" },
DIAGNOSED: { kind: "diagnosis", label: "诊断" },
REPAIRING: { kind: "repair", label: "修复" },
VERIFYING: { kind: "repair", label: "复测" },
PASSED: { kind: "success", label: "恢复" },
FAILED: { kind: "fault", label: "失败" },
};
const FALLBACK_SCENARIOS = [
["FI-FIELD-01", "位置标定丢失", "座椅移动 / 位置记忆"],
["FI-FIELD-02", "位置记录CRC回退", "四轴位置 / SMV8持久化"],
["FI-FIELD-03", "瞬态过载误判", "电机移动 / 防夹保护"],
["FI-FIELD-04", "热管理唤醒失败", "通风 / 加热"],
["FI-FIELD-05", "风扇关闭异常", "座椅通风"],
["FI-FIELD-06", "WDT复位后失能", "座椅控制器 / 多功能协同"],
].map(([id, shortTitle, system]) => ({
id,
short_title: shortTitle,
system,
normal_task: "等待连接后加载正常任务",
fault_reaction: "等待连接后加载故障停止状态",
diagnosis: "等待连接后加载 AI 诊断",
repair: "等待连接后加载恢复策略",
recovery_reaction: "等待连接后加载恢复结果",
field_basis: "自修复服务连接后显示实时用例说明",
}));
function fallbackState(message = "正在连接自修复服务") {
return {
scenarios: FALLBACK_SCENARIOS,
healing: {
phase: "IDLE",
phase_text: "等待服务连接",
elapsed_s: 0,
case_id: null,
telemetry: {},
events: [],
},
batch: { active: false, completed: 0, total: 6, passed: 0, failed: 0, results: [] },
runtime: {
active: false,
execution_mode: "shadow",
started_epoch: null,
stopped_epoch: null,
elapsed_s: 0,
healthy_elapsed_s: 0,
motion_profile: "四电机协同自由运动",
cycle_count: 0,
injected_count: 0,
repaired_count: 0,
events: [],
},
bridge: { online: false, protocol_online: false, bus_traffic: false },
bridge_manager: { state: "STARTING", message },
integration: { can_runtime_configured: false, firmware_tools_configured: false },
hardware: { groups: [], connectors: {} },
hardware_session: { active: false, events: [] },
bench_twin: {},
};
}
const $ = (selector, root = document) => root.querySelector(selector);
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
const esc = (value) => String(value == null ? "" : value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
let latest = fallbackState();
let selectedCaseId = null;
let selectedPinId = "JC1-1";
let activeView = sessionStorage.getItem(VIEW_KEY) === "hardware"
? "hardware"
: "software";
let executionMode = sessionStorage.getItem(MODE_KEY) === "bench"
? "bench"
: "shadow";
let polling = false;
let toastTimer = 0;
let confirmationResolve = null;
let eventFilter = "all";
function isAdmin() {
return window.CognitiveAgentConsoleV2?.getRole() === "admin"
|| document.body.dataset.demoRole === "admin";
}
function isActive() {
return isAdmin() && document.body.dataset.module === "self-repair";
}
function toast(message, error = false) {
const node = $("#toast");
if (!node) return;
node.textContent = message;
node.className = error ? "toast show error" : "toast show";
window.clearTimeout(toastTimer);
toastTimer = window.setTimeout(() => { node.className = "toast"; }, 3600);
}
function buildShell() {
const sidebar = $(".v2-sidebar");
const mainPanel = $(".main-panel");
const moduleSwitcher = $("#v2ModuleSwitcher");
if (!sidebar || !mainPanel || !moduleSwitcher || $("#v2SelfRepair")) return false;
const repairSidebar = document.createElement("section");
repairSidebar.className = "v2-sidebar-section v2-repair-sidebar";
repairSidebar.innerHTML = `
SELF-REPAIR DESK
控制台
修复对象
配置信息
影子仿真不会写入 ECU 或驱动实体设备
`;
sidebar.insertBefore(repairSidebar, $(".v2-sidebar-footer"));
const repairFooter = document.createElement("footer");
repairFooter.className = "v2-sidebar-footer v2-repair-footer offline";
repairFooter.innerHTML = `
自修复安全链路CAN 桥接离线
`;
sidebar.append(repairFooter);
const root = document.createElement("section");
root.className = "v2-self-repair";
root.id = "v2SelfRepair";
root.setAttribute("aria-label", "座椅 AI 自修复验证台");
root.innerHTML = `
正在构建座椅孪生坐垫 · 靠背 · 腿托 · 滑轨
拖动旋转 · 滚轮缩放 · 双击复位
影子动作仿真等待启动座椅任务待机
实际动作将同步显示在座椅模型
FAULT INJECTION
量产故障注入
6 个用例
先启动座椅持续运行,再按需选择故障。注入事件会明确标记为管理员操作。
本次连续运行00:00:00
累计健康运行00:00:00
故障 / 已修复0 / 0
FUNCTION BRANCHES
功能支路诊断
等待诊断
`;
moduleSwitcher.insertAdjacentElement("afterend", root);
const select = $("#repairExecutionMode");
select.value = executionMode;
window.CognitiveAgentConsoleV2?.enhanceSelect(select);
return true;
}
async function apiPost(path, body = {}) {
const response = await fetch(`${API_ROOT}${path}`, {
method: "POST",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-Cognitive-Agent-Role": "admin",
},
body: JSON.stringify(body),
});
const value = await response.json().catch(() => ({ message: `HTTP ${response.status}` }));
if (!response.ok || value.ok === false) {
throw new Error(value.message || value.error || `HTTP ${response.status}`);
}
return value;
}
function showView(view) {
activeView = view === "hardware" ? "hardware" : "software";
sessionStorage.setItem(VIEW_KEY, activeView);
$$('[data-repair-view]').forEach((button) => {
const active = button.dataset.repairView === activeView;
button.classList.toggle("active", active);
button.setAttribute("aria-pressed", String(active));
});
$$(".v2-repair-view").forEach((page) => {
const active = page.dataset.repairPage === activeView;
page.hidden = !active;
page.classList.toggle("active", active);
});
window.setTimeout(() => {
window.dispatchEvent(new Event("resize"));
if (activeView === "software") {
window.dispatchEvent(new Event("self-repair-seat-reset"));
}
}, 80);
}
function scenarioForDisplay() {
if (!latest?.scenarios?.length) return null;
return latest.scenarios.find((item) => item.id === selectedCaseId)
|| latest.scenarios[0];
}
function formatDuration(seconds) {
const whole = Math.max(0, Math.floor(Number(seconds || 0)));
const hours = Math.floor(whole / 3600);
const minutes = Math.floor((whole % 3600) / 60);
const remainder = whole % 60;
return [hours, minutes, remainder]
.map((value) => String(value).padStart(2, "0"))
.join(":");
}
function formatEventTime(epoch) {
if (!epoch) return "--:--:--.---";
const value = new Date(Number(epoch) * 1000);
const base = [value.getHours(), value.getMinutes(), value.getSeconds()]
.map((item) => String(item).padStart(2, "0"))
.join(":");
return `${base}.${String(value.getMilliseconds()).padStart(3, "0")}`;
}
function runtimeState() {
return latest.runtime || fallbackState().runtime;
}
function renderEventStream() {
const runtime = runtimeState();
const healing = latest.healing || {};
const scenario = (latest.scenarios || []).find((item) => item.id === healing.case_id)
|| scenarioForDisplay();
const runtimeEvents = (runtime.events || []).map((event) => ({
...event,
kind: event.kind || "system",
label: event.label || "运行",
}));
const healingEvents = (healing.events || []).map((event) => {
const meta = EVENT_META[event.phase] || EVENT_META.BASELINE;
const injected = event.phase === "INJECTED";
return {
...event,
kind: meta.kind,
label: meta.label,
title: injected
? `管理员注入「${scenario?.short_title || "故障用例"}」`
: event.title,
tags: [
injected ? "来源:admin" : null,
healing.run_id ? `事件:${healing.run_id}` : null,
healing.diagnosed_code && ["DIAGNOSED", "REPAIRING", "VERIFYING", "PASSED", "FAILED"].includes(event.phase)
? `诊断码:${healing.diagnosed_code}` : null,
].filter(Boolean),
};
});
const activeRepair = ["INJECTED", "DIAGNOSED", "REPAIRING", "VERIFYING"].includes(healing.phase);
const twin = latest.bench_twin || {};
const naturalFault = runtime.active && !activeRepair && ["IDLE", "PASSED"].includes(healing.phase)
&& Number(twin.system?.faulted_mask || 0) !== 0;
const liveEvents = runtime.active && !activeRepair ? [{
epoch: latest.generated_epoch,
time: formatEventTime(latest.generated_epoch),
kind: naturalFault ? "fault" : "heartbeat",
label: naturalFault ? "自然故障" : "运行心跳",
title: naturalFault
? "运行中检测到座椅异常"
: `座椅已正常运行 ${formatDuration(runtime.healthy_elapsed_s)}`,
detail: naturalFault
? `ECU FaultedMask=0x${Number(twin.system.faulted_mask).toString(16).toUpperCase()},AI正在持续监测。`
: `${runtime.motion_profile} · 已完成 ${runtime.cycle_count || 0} 个动作周期。`,
tags: (twin.axes?.length ? twin.axes : [35, 42, 48, 20].map((position_pct, motor) => ({ motor, position_pct })))
.map((axis) => `M${axis.motor} ${Number(axis.position_pct || 0).toFixed(0)}%`),
}] : [];
const events = [...runtimeEvents, ...healingEvents, ...liveEvents]
.filter((event) => eventFilter === "all" || ["fault", "diagnosis", "repair", "success"].includes(event.kind))
.sort((left, right) => Number(left.epoch || 0) - Number(right.epoch || 0));
$("#repairEventStream").innerHTML = events.length
? events.map((event) => `${esc(event.label)}${esc(event.title)}${esc(event.detail || "状态已更新")}
${event.tags?.length ? `
` : ""}
`).join("")
: `${runtime.active ? "座椅正在正常运行" : "等待启动座椅运行"}${runtime.active ? "运行心跳、故障、诊断和修复事件将在这里持续追加。" : "启动后将记录运行时长、四轴状态和后续故障注入。"}
`;
$("#repairEventStream").scrollTop = $("#repairEventStream").scrollHeight;
$("#repairRuntimeElapsed").textContent = formatDuration(runtime.elapsed_s);
$("#repairHealthyElapsed").textContent = formatDuration(runtime.healthy_elapsed_s);
$("#repairFaultSummary").textContent = `${Number(runtime.injected_count || 0)} / ${Number(runtime.repaired_count || 0)}`;
const failed = healing.phase === "FAILED";
const repairing = activeRepair;
const logState = $("#repairLogState");
logState.textContent = failed ? "需要处理" : repairing ? "自修复中" : healing.phase === "PASSED" && runtime.active ? "已恢复运行" : runtime.active ? "健康运行" : "等待启动";
logState.dataset.state = failed ? "fault" : repairing ? "repairing" : runtime.active ? "healthy" : "idle";
}
function renderCases() {
if (!selectedCaseId && latest.scenarios?.length) selectedCaseId = latest.scenarios[0].id;
const results = new Map((latest.batch?.results || []).map((item) => [item.case_id, item]));
$("#repairCaseList").innerHTML = (latest.scenarios || []).map((scenario, index) => {
const result = results.get(scenario.id);
const running = latest.batch?.current_case_id === scenario.id
|| (latest.healing?.case_id === scenario.id
&& !["IDLE", "PASSED", "FAILED"].includes(latest.healing.phase));
const state = result?.result === "PASS" ? "complete"
: result?.result === "FAIL" ? "failed"
: running ? "active" : "pending";
return ``;
}).join("");
$$('[data-repair-case]').forEach((button) => {
button.addEventListener("click", () => {
if (latest.batch?.active) return;
selectedCaseId = button.dataset.repairCase;
renderCases();
renderTwin();
});
});
const selected = scenarioForDisplay();
const runtime = runtimeState();
$("#repairCaseCount").textContent = `${(latest.scenarios || []).length} 个用例`;
$("#repairSelectedCase").textContent = selected
? `当前选择:${selected.short_title}` : "当前选择:—";
$("#repairInjectionCount").textContent = Number(runtime.injected_count || 0)
? `本次已注入 ${runtime.injected_count} 次 · 已修复 ${runtime.repaired_count || 0} 次`
: "本次尚未注入故障";
const activeRepair = !["IDLE", "PASSED", "FAILED"].includes(latest.healing?.phase);
$("#repairInject").disabled = !runtime.active || activeRepair || latest.batch?.active;
$("#repairInject").innerHTML = `${activeRepair ? "自修复处理中" : runtime.active ? "注入所选故障" : "请先启动座椅运行"}`;
}
function renderTwin() {
const healing = latest.healing;
const scenario = scenarioForDisplay();
if (!scenario) return;
const runtime = runtimeState();
const telemetry = healing.telemetry || {};
const live = latest.bench_twin || {};
const bench = executionMode === "bench";
const positions = Array.isArray(telemetry.axis_positions)
? telemetry.axis_positions : [35, 42, 48, 20];
const axes = bench && live.axes?.length === 4
? live.axes
: positions.map((position, motor) => ({
motor,
label: ["水平滑轨", "坐盆升降", "靠背角度", "腿托角度"][motor],
position_pct: Number(position),
active: (telemetry.active_axes || []).includes(motor),
position_valid: telemetry.position_valid !== false,
}));
const stage = {
NORMAL_TASK: "正常任务",
FAULT_REACTION: "故障停止",
REPAIR: "执行修复",
VERIFYING: "安全复测",
RESUMING: "继续原任务",
COMPLETE: "任务已恢复",
SAFE_STOP: "安全停止",
}[telemetry.operation_stage] || healing.phase_text;
$("#repairTwinSource").textContent = bench
? (live.online ? "实物 CAN 同步" : "实物 CAN 暂离线")
: "影子动作仿真";
$("#repairTwinTask").textContent = healing.phase === "IDLE"
? (runtime.active ? runtime.motion_profile : "等待启动座椅运行")
: scenario.normal_task;
$("#repairTwinStage").textContent = healing.phase === "IDLE" && runtime.active
? "自由运动" : stage || "待机";
$("#repairTwinState").textContent = healing.phase === "IDLE"
? (runtime.active ? "持续运行中" : "等待启动")
: healing.phase === "PASSED" && runtime.active ? "已恢复运行" : healing.phase_text;
$("#repairTwinReaction").textContent = telemetry.bench_reaction
|| (healing.phase === "IDLE"
? (runtime.active
? `四电机按设定轨迹协同运动 · 已完成 ${runtime.cycle_count || 0} 个周期`
: "实际动作将同步显示在座椅模型")
: scenario.fault_reaction);
$("#repairAxisStrip").innerHTML = axes.map((axis) => {
const position = Math.max(0, Math.min(100, Number(axis.position_pct || 0)));
return `M${axis.motor}${position.toFixed(0)}%`;
}).join("");
window.dispatchEvent(new CustomEvent("seat-healing-state", { detail: {
state: healing.phase,
axes: axes.map((axis) => ({
motor: axis.motor,
position_pct: Number(axis.position_pct || 0),
active: Boolean(axis.active),
valid: axis.position_valid !== false,
})),
ventilation: Boolean((bench ? live.thermal : telemetry).fan_running),
heatingLevel: Number((bench ? live.thermal : telemetry).heater_level || 0),
} }));
}
function connectorRows(name, pins) {
if (name === "JA1") return Array.from({ length: 7 }, (_, index) => [index + 1, index + 8]);
if (name === "JB1") return Array.from({ length: 9 }, (_, index) => [(index + 1) * 2, index * 2 + 1]);
return Array.from({ length: 20 }, (_, index) => [index + 1, index + 21]);
}
function pinButton(pin) {
if (!pin) return "";
const state = pin.enabled === false ? "NOT_USED" : pin.state;
return ``;
}
function renderConnectors(hardware) {
const connectors = hardware.connectors || {};
const names = ["JA1", "JB1", "JC1"];
$("#repairConnectors").innerHTML = names.map((name) => {
const pins = connectors[name] || [];
const byNumber = new Map(pins.map((pin) => [Number(pin.number), pin]));
const ok = pins.filter((pin) => pin.state === "OK").length;
const abnormal = pins.filter((pin) => ["WARN", "FAULT"].includes(pin.state)).length;
return `${name}已验 ${ok} · 异常 ${abnormal}${name === "JC1" ? "
" : ""}
${connectorRows(name, pins).map((row) => `${pinButton(byNumber.get(row[0]))}${pinButton(byNumber.get(row[1]))}`).join("")}
`;
}).join("");
$$('[data-repair-pin]').forEach((button) => button.addEventListener("click", () => {
selectedPinId = button.dataset.repairPin;
renderHardware();
}));
}
function renderPinDetail(hardware) {
const allPins = Object.values(hardware.connectors || {}).flat();
const pin = allPins.find((item) => item.id === selectedPinId) || allPins[0];
if (!pin) return;
selectedPinId = pin.id;
const state = pin.enabled === false ? "NOT_USED" : pin.state;
$("#repairPinTitle").textContent = pin.id;
$("#repairPinState").textContent = STATE_TEXT[state] || state;
$("#repairPinState").dataset.state = state;
$("#repairPinDetail").innerHTML = `
- 原理图网络
- ${esc(pin.net)}
- 座椅侧功能
- ${esc(pin.function)}
- 板内二级溯源
- ${esc(pin.mcu || "—")}
- 本工程启用
- ${pin.enabled === false ? "否" : "是"}
判定依据
${(pin.evidence || ["等待诊断证据"]).map((item) => `- ${esc(item)}
`).join("")}
建议检查顺序
${(pin.actions || ["启动硬件诊断后生成建议"]).map((item) => `- ${esc(item)}
`).join("")}
`;
}
function renderHardware() {
const hardware = latest.hardware || {};
const session = latest.hardware_session || {};
const bridge = latest.bridge || {};
const manager = latest.bridge_manager || {};
const adapterOnline = hardware.link?.adapter === "OK"
|| Boolean(bridge.bus?.opened)
|| (bridge.online && bridge.bus_traffic);
const layers = [
["1", "CAN Runtime", bridge.online ? "正常" : "离线", bridge.online ? "OK" : "UNKNOWN"],
["2", "CAN设备 / 通道", adapterOnline ? "设备与通道已打开" : "未连接", adapterOnline ? "OK" : "UNKNOWN"],
["3", "总线实时流量", bridge.bus_traffic ? "正常" : "未检测", bridge.bus_traffic ? "OK" : "WARN"],
["4", "座椅ECU协议", bridge.protocol_online ? "0x301~0x305在线" : "未检测", bridge.protocol_online ? "OK" : "UNKNOWN"],
];
$("#repairLinkLayers").innerHTML = layers.map((item) => `${item[0]} · ${item[1]}${item[2]}
`).join("");
$("#repairBridgeMessage").textContent = manager.message || "等待桥接状态";
$("#repairLinkDetail").textContent = bridge.protocol_online
? `协议在线 · 最近帧 ${(bridge.recent_frame_ids || []).join(" / ")}`
: bridge.bus_traffic
? "CAN 有实时流量,但尚未识别本工程 0x301~0x305 协议。"
: "尚未收到实时 CAN 帧,启用针脚保持未检测状态,不表示针脚故障。";
$("#repairHardwareGroups").innerHTML = (hardware.groups || []).map((group) => ``).join("");
$("#repairHardwareOverall").textContent = `已验证 ${hardware.connector_ok_count || 0}/21 · 异常 ${(hardware.connector_fault_count || 0) + (hardware.connector_warn_count || 0)}`;
const toggle = $("#repairHardwareToggle");
toggle.innerHTML = session.active
? '停止硬件诊断'
: '启动硬件诊断';
renderConnectors(hardware);
renderPinDetail(hardware);
const events = session.events || [];
$("#repairHardwareRecordsBody").innerHTML = `${esc(session.report_path || "停止诊断后自动保存硬件报告")}
${events.length ? `| 时间 | 支路 | 状态 | 摘要 |
${[...events].reverse().map((event) => `| ${esc(event.time)} | ${esc(event.name)} | ${esc(STATE_TEXT[event.state] || event.state)} | ${esc(event.summary)} |
`).join("")}
` : "尚无硬件诊断事件。
"}`;
}
function renderGlobal() {
const bridge = latest.bridge || {};
const manager = latest.bridge_manager || {};
const healing = latest.healing || {};
const hardwarePhase = latest.hardware_session?.progress?.phase;
const integration = latest.integration || {};
const runtime = runtimeState();
$("#repairCanStatus").textContent = bridge.online ? "在线" : "离线";
$("#repairCanDot").dataset.state = bridge.online ? "online" : manager.state === "STARTING" ? "working" : "offline";
$("#repairProtocolStatus").textContent = bridge.protocol_online ? "协议在线" : "未检测";
$("#repairProtocolDot").dataset.state = bridge.protocol_online ? "online" : "offline";
const hardwareState = {
BASELINE: ["硬件检测中", "working"],
RUNNING: ["硬件检测中", "working"],
PASSED: ["21针通过", "online"],
FAILED: ["硬件异常", "offline"],
}[hardwarePhase];
const aiState = activeView === "hardware" && hardwareState
? hardwareState
: runtime.active && healing.phase === "IDLE"
? ["持续监控", "online"]
: runtime.active && healing.phase === "PASSED"
? ["已恢复监控", "online"]
: [healing.phase_text || "等待验证", healing.phase === "PASSED"
? "online"
: healing.phase === "IDLE" ? "offline" : "working"];
$("#repairAiStatus").textContent = aiState[0];
$("#repairAiDot").dataset.state = aiState[1];
$("#repairConnectionText").textContent = bridge.online ? "CAN 桥接在线" : manager.message || "CAN 桥接离线";
$(".v2-repair-footer").classList.toggle("offline", !bridge.online);
const benchOption = $('#repairExecutionMode option[value="bench"]');
if (benchOption) benchOption.disabled = !integration.can_runtime_configured;
if (executionMode === "bench" && !integration.can_runtime_configured) {
executionMode = "shadow";
$("#repairExecutionMode").value = "shadow";
sessionStorage.setItem(MODE_KEY, executionMode);
}
$("#repairExecutionState").textContent = executionMode === "bench" ? "HIL" : "SAFE";
$("#repairConfigNote").textContent = executionMode === "bench"
? "真实台架动作受本机 admin、安全确认与桥接状态约束"
: "影子仿真不会写入 ECU 或驱动实体设备";
$("#repairFirmwareFlash").disabled = !integration.firmware_tools_configured;
const runtimeToggle = $("#repairRuntimeToggle");
const repairActive = ["INJECTED", "DIAGNOSED", "REPAIRING", "VERIFYING"].includes(healing.phase);
runtimeToggle.innerHTML = runtime.active
? '停止座椅运行'
: '启动座椅运行';
runtimeToggle.classList.toggle("danger", runtime.active);
runtimeToggle.classList.toggle("primary", !runtime.active);
runtimeToggle.disabled = repairActive;
window.CognitiveAgentConsoleV2?.enhanceSelect($("#repairExecutionMode"));
}
function renderRecords() {
const results = latest.batch?.results || [];
const events = latest.healing?.events || [];
$("#repairSoftwareRecordsBody").innerHTML = `${esc(latest.latest_report_path || "成功用例将自动保存诊断快照")}
${results.length ? `| 用例 | 结果 | 耗时 | 诊断码 |
${results.map((item) => `| ${esc(item.title)} | ${esc(item.result)} | ${Number(item.duration_s || 0).toFixed(2)} s | ${esc(item.diagnosed_code || "—")} |
`).join("")}
` : "尚无批量验证记录。
"}${events.map((event) => `
${esc(event.title)}${esc(event.detail)}`).join("")}
`;
}
function renderAll() {
renderGlobal();
renderCases();
renderEventStream();
renderTwin();
renderHardware();
renderRecords();
// Polling refreshes data only. Reapplying the active view here would also
// reset the user's current 3D orbit every 600 ms.
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
}
async function poll() {
if (!isActive() || polling) return;
polling = true;
try {
const response = await fetch(`${API_ROOT}/state`, {
cache: "no-store",
credentials: "same-origin",
headers: { Accept: "application/json", "X-Cognitive-Agent-Role": "admin" },
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
latest = await response.json();
if (!["IDLE"].includes(latest.healing?.phase)) selectedCaseId = latest.healing.case_id;
renderAll();
} catch (error) {
$("#repairConnectionText").textContent = `自修复服务连接失败:${error.message}`;
$(".v2-repair-footer").classList.add("offline");
} finally {
polling = false;
}
}
async function runAction(action, successMessage) {
try {
await action();
toast(successMessage);
await poll();
} catch (error) {
toast(error.message, true);
}
}
function finishConfirmation(accepted) {
const dialog = $("#repairConfirmDialog");
const resolve = confirmationResolve;
confirmationResolve = null;
if (dialog?.open) dialog.close(accepted ? "confirmed" : "cancelled");
if (resolve) resolve(accepted);
}
function confirmAction(message, confirmLabel = "确认执行") {
const dialog = $("#repairConfirmDialog");
if (!dialog || confirmationResolve) return Promise.resolve(false);
$("#repairConfirmMessage").textContent = message;
$("#repairConfirmAccept").textContent = confirmLabel;
dialog.showModal();
return new Promise((resolve) => {
confirmationResolve = resolve;
});
}
function bindEvents() {
$$('[data-repair-view]').forEach((button) => button.addEventListener("click", () => showView(button.dataset.repairView)));
$("#repairExecutionMode").addEventListener("change", (event) => {
executionMode = event.target.value === "bench" ? "bench" : "shadow";
sessionStorage.setItem(MODE_KEY, executionMode);
if (latest) renderAll();
});
$("#repairRuntimeToggle").addEventListener("click", async () => {
const active = Boolean(runtimeState().active);
if (executionMode === "bench" && !(await confirmAction(
active
? "即将停止实物座椅连续运行。请确认可安全停止。"
: "即将启动实物座椅四电机协同自由运动。请确认运动区域无人、无遮挡且急停可用。",
active ? "确认停止" : "确认启动",
))) return;
runAction(
() => apiPost("/runtime", { active: !active, execution_mode: executionMode }),
active ? "座椅连续运行已停止" : "座椅连续运行已启动",
);
});
$("#repairInject").addEventListener("click", async () => {
const scenario = scenarioForDisplay();
if (!runtimeState().active || !scenario) return;
if (executionMode === "bench" && !(await confirmAction(
`即将向实物 ECU 注入「${scenario.short_title}」并驱动座椅自修复。请确认运动区域无人、无遮挡且急停可用。`,
"确认注入",
))) return;
runAction(
() => apiPost("/healing/start", { case_id: selectedCaseId, execution_mode: executionMode }),
executionMode === "bench" ? "实物故障已注入" : "影子故障已注入,开始自修复",
);
});
$("#repairReset").addEventListener("click", async () => {
runAction(() => apiPost("/runtime/reset-statistics"), "本次运行统计已重置");
});
$("#repairFirmwareFlash").addEventListener("click", async () => {
if (!(await confirmAction("即将构建并烧录台架测试固件。请确认座椅运动区域无人、急停可用且 CMSIS-DAP 已连接。", "确认构建/烧录"))) return;
runAction(() => apiPost("/firmware/build-flash", { confirmed_bench_clear: true }), "固件构建/烧录流程已完成");
});
$("#repairHardwareToggle").addEventListener("click", async () => {
if (!latest) return;
if (latest.hardware_session?.active) {
runAction(() => apiPost("/hardware/stop"), "硬件诊断已停止并保存报告");
return;
}
if (!(await confirmAction("硬件诊断可能自动烧录测试固件,并短时驱动四路电机、风扇与低档加热。请确认台架区域安全且急停可用。", "确认启动诊断"))) return;
runAction(() => apiPost("/hardware/start", { confirmed_bench_clear: true }), "21 针主动硬件诊断已启动");
});
$("#repairHardwareSelfTest").addEventListener("click", () => runAction(() => apiPost("/hardware/self-test"), "72 针映射和诊断规则自检完成"));
$("#repairEmergencyStop").addEventListener("click", async () => {
if (!(await confirmAction("确认提交 STOP ALL 和热管理关闭请求?", "确认紧急停止"))) return;
runAction(() => apiPost("/emergency-stop"), "紧急停止请求已提交");
});
$("#repairConfirmCancel").addEventListener("click", () => finishConfirmation(false));
$("#repairConfirmAccept").addEventListener("click", () => finishConfirmation(true));
$("#repairConfirmDialog").addEventListener("cancel", (event) => {
event.preventDefault();
finishConfirmation(false);
});
$("#repairConfirmDialog").addEventListener("close", () => {
if (confirmationResolve) finishConfirmation(false);
});
$("#repairSoftwareRecords").addEventListener("click", () => $("#repairSoftwareDialog").showModal());
$("#repairHardwareRecords").addEventListener("click", () => $("#repairHardwareDialog").showModal());
$$('[data-repair-log-filter]').forEach((button) => button.addEventListener("click", () => {
eventFilter = button.dataset.repairLogFilter === "fault" ? "fault" : "all";
$$('[data-repair-log-filter]').forEach((item) => item.classList.toggle("active", item === button));
renderEventStream();
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
}));
$$('[data-repair-close]').forEach((button) => button.addEventListener("click", () => $(`#${button.dataset.repairClose}`).close()));
window.addEventListener("cognitive-agent:module-change", (event) => {
if (event.detail?.module === "self-repair") poll();
});
window.addEventListener("cognitive-agent:demo-role-change", () => {
if (isActive()) poll();
});
}
function init() {
if (!buildShell()) return;
bindEvents();
showView(activeView);
renderAll();
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
if (isActive()) poll();
window.setInterval(poll, 600);
}
document.addEventListener("DOMContentLoaded", init);
}());