617 lines
34 KiB
JavaScript
617 lines
34 KiB
JavaScript
(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 PHASE_INDEX = {
|
||
IDLE: -1,
|
||
BASELINE: 0,
|
||
INJECTED: 1,
|
||
DIAGNOSED: 2,
|
||
REPAIRING: 3,
|
||
VERIFYING: 4,
|
||
PASSED: 5,
|
||
FAILED: 2,
|
||
};
|
||
const STATE_TEXT = {
|
||
OK: "正常",
|
||
WARN: "告警",
|
||
FAULT: "故障",
|
||
UNKNOWN: "未检测",
|
||
UNMONITORED: "关联待测",
|
||
NOT_USED: "未启用",
|
||
IDLE: "检测中",
|
||
};
|
||
const TIMELINE = [
|
||
{ title: "开始正常任务", phases: ["BASELINE"] },
|
||
{ title: "故障发生并停止", phases: ["INJECTED"] },
|
||
{ title: "AI判断原因", phases: ["DIAGNOSED"] },
|
||
{ title: "执行恢复策略", phases: ["REPAIRING"] },
|
||
{ title: "继续原任务", phases: ["VERIFYING", "PASSED"] },
|
||
];
|
||
|
||
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 = null;
|
||
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;
|
||
|
||
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 = `
|
||
<div class="v2-section-heading"><span>SELF-REPAIR DESK</span><h2>控制台</h2></div>
|
||
<span class="v2-field-caption">修复对象</span>
|
||
<div class="v2-repair-view-switch" role="group" aria-label="修复对象">
|
||
<button type="button" data-repair-view="software"><i data-lucide="shield-check"></i><span><strong>软件修复</strong><small>注入 · 判断 · 修复 · 验证</small></span></button>
|
||
<button type="button" data-repair-view="hardware"><i data-lucide="cable"></i><span><strong>硬件诊断</strong><small>CAN · 支路 · 连接器针脚</small></span></button>
|
||
</div>
|
||
<div class="v2-divider"></div>
|
||
<span class="v2-field-caption">配置信息</span>
|
||
<div class="v2-repair-config-stack">
|
||
<div class="v2-repair-config-card"><i data-lucide="car-front"></i><div><small>当前车型</small><strong>腾势 N7</strong></div><span>N7</span></div>
|
||
<div class="v2-repair-config-card"><i data-lucide="sparkles"></i><div><small>推理模型</small><strong>GPT-5.6-sol</strong></div><span>READY</span></div>
|
||
<div class="v2-repair-config-card"><i data-lucide="cpu"></i><div><small>座椅控制器</small><strong>T2G 4BF</strong></div><span>4BF</span></div>
|
||
<div class="v2-repair-config-card v2-config-select-card repair-execution-card">
|
||
<i data-lucide="workflow"></i>
|
||
<div class="v2-config-card-copy"><small>验证方式</small><div class="v2-config-card-select">
|
||
<select id="repairExecutionMode" aria-label="选择自修复验证方式">
|
||
<option value="shadow">影子仿真</option>
|
||
<option value="bench">实物台架</option>
|
||
</select>
|
||
</div></div><span id="repairExecutionState">SAFE</span>
|
||
</div>
|
||
</div>
|
||
<p class="v2-repair-config-note" id="repairConfigNote">影子仿真不会写入 ECU 或驱动实体设备</p>`;
|
||
sidebar.insertBefore(repairSidebar, $(".v2-sidebar-footer"));
|
||
|
||
const repairFooter = document.createElement("footer");
|
||
repairFooter.className = "v2-sidebar-footer v2-repair-footer offline";
|
||
repairFooter.innerHTML = `
|
||
<span class="v2-live-dot"></span>
|
||
<div><strong>自修复安全链路</strong><small id="repairConnectionText">CAN 桥接离线</small></div>
|
||
<button type="button" id="repairEmergencyStop" title="STOP ALL + 热管理关闭" aria-label="自修复紧急停止"><i data-lucide="octagon"></i></button>`;
|
||
sidebar.append(repairFooter);
|
||
|
||
const root = document.createElement("section");
|
||
root.className = "v2-self-repair";
|
||
root.id = "v2SelfRepair";
|
||
root.setAttribute("aria-label", "座椅 AI 自修复验证台");
|
||
root.innerHTML = `
|
||
<header class="v2-repair-heading">
|
||
<div><span class="section-kicker">COGNITIVE SEAT / SELF-REPAIR</span><h1>座椅AI自修复验证台</h1><p>量产软件故障闭环恢复 · 开发板接口在线诊断</p></div>
|
||
<div class="v2-repair-status-grid">
|
||
<div><i id="repairCanDot"></i><span><small>CAN桥接</small><strong id="repairCanStatus">离线</strong></span></div>
|
||
<div><i id="repairProtocolDot"></i><span><small>座椅ECU</small><strong id="repairProtocolStatus">未检测</strong></span></div>
|
||
<div><i id="repairAiDot"></i><span><small>AI状态</small><strong id="repairAiStatus">等待验证</strong></span></div>
|
||
</div>
|
||
</header>
|
||
|
||
<section class="v2-repair-view" data-repair-page="software">
|
||
<header class="v2-repair-toolbar">
|
||
<div><span>01</span><div><h2>软件自修复</h2><p>正常任务 → 故障停止 → AI 判断 → 安全修复 → 继续原目标</p></div></div>
|
||
<div>
|
||
<button type="button" id="repairSoftwareRecords"><i data-lucide="history"></i>验证记录</button>
|
||
<button type="button" id="repairFirmwareFlash"><i data-lucide="microchip"></i>构建/烧录</button>
|
||
<button type="button" id="repairReset"><i data-lucide="rotate-ccw"></i>重置归零</button>
|
||
<button type="button" id="repairRunAll"><i data-lucide="list-checks"></i>批量验证6项</button>
|
||
<button class="primary" type="button" id="repairStart"><i data-lucide="play"></i>启动当前用例</button>
|
||
</div>
|
||
</header>
|
||
<div class="v2-repair-software-grid">
|
||
<section class="v2-repair-panel v2-repair-cases-panel">
|
||
<header><div><span class="section-kicker">PRODUCTION CASES</span><h2>量产故障用例</h2></div><b id="repairBatchCount">0 / 6</b></header>
|
||
<div class="v2-repair-overall-progress" aria-label="六项用例总体进度"><span id="repairBatchProgress"></span></div>
|
||
<p id="repairBatchSummary">等待启动批量验证</p>
|
||
<div class="v2-repair-case-list" id="repairCaseList"></div>
|
||
</section>
|
||
<section class="v2-repair-panel v2-repair-twin-panel">
|
||
<header><div><span class="section-kicker">DIGITAL TWIN</span><h2>座椅3D孪生</h2></div><b id="repairTwinState">正常基线</b></header>
|
||
<div class="v2-repair-seat-viewer" id="repairSeatViewer">
|
||
<canvas id="repairSeatCanvas" aria-label="自修复座椅三维孪生,可拖动旋转并滚轮缩放"></canvas>
|
||
<div class="v2-repair-model-loading" id="repairModelLoading"><span></span><strong>正在构建座椅孪生</strong><small>坐垫 · 靠背 · 腿托 · 滑轨</small></div>
|
||
<div class="v2-repair-orbit-hint">拖动旋转 · 滚轮缩放 · 双击复位</div>
|
||
<div class="v2-repair-twin-live"><div><small id="repairTwinSource">影子动作仿真</small><strong id="repairTwinTask">等待启动座椅任务</strong><b id="repairTwinStage">待机</b></div><div id="repairAxisStrip"></div><p id="repairTwinReaction">实际动作将同步显示在座椅模型</p></div>
|
||
</div>
|
||
</section>
|
||
<section class="v2-repair-panel v2-repair-timeline-panel">
|
||
<header><div><span class="section-kicker">HEALING FLOW</span><h2>座椅任务自修复</h2></div><b id="repairElapsed">0.000 s</b></header>
|
||
<div class="v2-repair-timeline" id="repairTimeline"></div>
|
||
</section>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="v2-repair-view" data-repair-page="hardware" hidden>
|
||
<header class="v2-repair-toolbar">
|
||
<div><span>02</span><div><h2>硬件自诊断</h2><p>固件资格检查 → CAN 分层诊断 → 21 个启用针脚主动检测</p></div></div>
|
||
<div>
|
||
<button type="button" id="repairHardwareSelfTest"><i data-lucide="list-checks"></i>规则自检</button>
|
||
<button type="button" id="repairHardwareRecords"><i data-lucide="history"></i>诊断记录</button>
|
||
<button class="primary" type="button" id="repairHardwareToggle"><i data-lucide="activity"></i>启动硬件诊断</button>
|
||
</div>
|
||
</header>
|
||
<section class="v2-repair-panel v2-repair-link-panel">
|
||
<header><div><span class="section-kicker">CAN LAYERS</span><h2>CAN分层诊断</h2></div><p id="repairBridgeMessage">等待桥接状态</p></header>
|
||
<div class="v2-repair-link-layers" id="repairLinkLayers"></div>
|
||
<p id="repairLinkDetail">尚未收到实时 CAN 帧,针脚保持未检测状态。</p>
|
||
</section>
|
||
<div class="v2-repair-hardware-grid">
|
||
<section class="v2-repair-panel v2-repair-groups-panel"><header><div><span class="section-kicker">FUNCTION BRANCHES</span><h2>功能支路诊断</h2></div><b id="repairHardwareOverall">等待诊断</b></header><div id="repairHardwareGroups"></div></section>
|
||
<section class="v2-repair-panel v2-repair-connectors-panel"><header><div><span class="section-kicker">CONNECTOR PINOUT</span><h2>JA1 / JB1 / JC1 全针脚视图</h2></div><div class="v2-repair-pin-legend"><span data-state="OK"></span>正常<span data-state="UNKNOWN"></span>未检测<span data-state="IDLE"></span>检测中<span data-state="WARN"></span>告警<span data-state="FAULT"></span>故障</div></header><div id="repairConnectors"></div></section>
|
||
<section class="v2-repair-panel v2-repair-pin-panel"><header><div><span class="section-kicker">PIN EVIDENCE</span><h2 id="repairPinTitle">JC1-1</h2></div><b id="repairPinState">未检测</b></header><div id="repairPinDetail"></div></section>
|
||
</div>
|
||
</section>
|
||
|
||
<dialog class="v2-repair-dialog" id="repairSoftwareDialog"><header><div><strong>软件自修复验证记录</strong><small>成功用例自动保存诊断快照</small></div><button type="button" data-repair-close="repairSoftwareDialog">×</button></header><div id="repairSoftwareRecordsBody"></div></dialog>
|
||
<dialog class="v2-repair-dialog" id="repairHardwareDialog"><header><div><strong>硬件自诊断记录</strong><small>支路状态变化与最终报告</small></div><button type="button" data-repair-close="repairHardwareDialog">×</button></header><div id="repairHardwareRecordsBody"></div></dialog>
|
||
<dialog class="v2-repair-dialog v2-repair-confirm-dialog" id="repairConfirmDialog" aria-labelledby="repairConfirmTitle">
|
||
<header><div><strong id="repairConfirmTitle">确认台架安全条件</strong><small>确认后才会向实物链路提交请求</small></div></header>
|
||
<div><p id="repairConfirmMessage"></p><div class="v2-repair-confirm-actions"><button type="button" id="repairConfirmCancel">取消</button><button class="primary" type="button" id="repairConfirmAccept">确认执行</button></div></div>
|
||
</dialog>`;
|
||
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 timelineDetail(index, scenario, healing) {
|
||
const telemetry = healing.telemetry || {};
|
||
const details = [
|
||
scenario.normal_task,
|
||
telemetry.bench_reaction || scenario.fault_reaction,
|
||
scenario.diagnosis,
|
||
scenario.repair,
|
||
scenario.recovery_reaction,
|
||
];
|
||
const phases = TIMELINE[index].phases;
|
||
const event = [...(healing.events || [])].reverse()
|
||
.find((item) => phases.includes(item.phase));
|
||
return event?.detail || event?.title || details[index] || "等待运行日志";
|
||
}
|
||
|
||
function renderTimeline() {
|
||
const healing = latest.healing;
|
||
const scenario = scenarioForDisplay();
|
||
if (!scenario) return;
|
||
const phaseIndex = PHASE_INDEX[healing.phase] ?? -1;
|
||
const passed = healing.phase === "PASSED";
|
||
const failed = healing.phase === "FAILED";
|
||
$("#repairTimeline").innerHTML = TIMELINE.map((stage, index) => {
|
||
let state = "pending";
|
||
if (passed || phaseIndex > index) state = "complete";
|
||
else if (phaseIndex === index && healing.phase !== "IDLE") state = failed ? "failed" : "active";
|
||
const label = state === "complete" ? "已完成" : state === "active" ? "进行中" : state === "failed" ? "失败" : "等待";
|
||
return `<article data-state="${state}"><i></i><div><header><strong>${esc(stage.title)}</strong><span>${label}</span></header><p>${esc(timelineDetail(index, scenario, healing))}</p></div></article>`;
|
||
}).join("");
|
||
$("#repairElapsed").textContent = `${Number(healing.elapsed_s || 0).toFixed(3)} s`;
|
||
}
|
||
|
||
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 `<button type="button" data-repair-case="${esc(scenario.id)}" data-state="${state}" class="${selectedCaseId === scenario.id ? "selected" : ""}"><span>${String(index + 1).padStart(2, "0")}</span><div><strong>${esc(scenario.short_title)}</strong><small>${esc(scenario.system)}</small></div><i></i></button>`;
|
||
}).join("");
|
||
$$('[data-repair-case]').forEach((button) => {
|
||
button.addEventListener("click", () => {
|
||
if (latest.batch?.active) return;
|
||
selectedCaseId = button.dataset.repairCase;
|
||
renderCases();
|
||
renderTimeline();
|
||
renderTwin();
|
||
});
|
||
});
|
||
const batch = latest.batch || {};
|
||
const completed = Number(batch.completed || 0);
|
||
const total = Number(batch.total || 6);
|
||
$("#repairBatchProgress").style.width = `${Math.min(100, (completed / Math.max(1, total)) * 100)}%`;
|
||
$("#repairBatchCount").textContent = `${completed} / ${total}`;
|
||
$("#repairBatchSummary").textContent = batch.active
|
||
? `正在验证 ${batch.current_case_id || "下一项"} · 通过 ${batch.passed} · 失败 ${batch.failed}`
|
||
: completed
|
||
? `批量验证完成 · 通过 ${batch.passed} · 失败 ${batch.failed}`
|
||
: "等待启动批量验证";
|
||
}
|
||
|
||
function renderTwin() {
|
||
const healing = latest.healing;
|
||
const scenario = scenarioForDisplay();
|
||
if (!scenario) return;
|
||
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"
|
||
? `等待启动:${scenario.normal_task}` : scenario.normal_task;
|
||
$("#repairTwinStage").textContent = stage || "待机";
|
||
$("#repairTwinState").textContent = healing.phase_text;
|
||
$("#repairTwinReaction").textContent = telemetry.bench_reaction
|
||
|| (healing.phase === "IDLE" ? scenario.field_basis : scenario.fault_reaction);
|
||
$("#repairAxisStrip").innerHTML = axes.map((axis) => {
|
||
const position = Math.max(0, Math.min(100, Number(axis.position_pct || 0)));
|
||
return `<span class="${axis.active ? "active" : ""}"><b>M${axis.motor}</b><i style="--position:${position}%"></i><em>${position.toFixed(0)}%</em></span>`;
|
||
}).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 "<span></span>";
|
||
const state = pin.enabled === false ? "NOT_USED" : pin.state;
|
||
return `<button type="button" data-repair-pin="${esc(pin.id)}" data-state="${esc(state)}" class="${selectedPinId === pin.id ? "selected" : ""}" aria-label="${esc(`${pin.id} ${pin.net} ${STATE_TEXT[state] || state}`)}"><b>${String(pin.number).padStart(2, "0")}</b><small>${esc(pin.net)}</small></button>`;
|
||
}
|
||
|
||
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 `<section class="v2-repair-connector" data-connector="${name}"><header><strong>${name}</strong><span>已验 ${ok} · 异常 ${abnormal}</span></header><div class="v2-repair-connector-body">${name === "JC1" ? "<i class=\"keying k1\"></i><i class=\"keying k2\"></i><i class=\"keying k3\"></i><i class=\"keying k4\"></i>" : ""}<div>${connectorRows(name, pins).map((row) => `<span>${pinButton(byNumber.get(row[0]))}${pinButton(byNumber.get(row[1]))}</span>`).join("")}</div></div></section>`;
|
||
}).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 = `
|
||
<dl><div><dt>原理图网络</dt><dd>${esc(pin.net)}</dd></div><div><dt>座椅侧功能</dt><dd>${esc(pin.function)}</dd></div><div><dt>板内二级溯源</dt><dd>${esc(pin.mcu || "—")}</dd></div><div><dt>本工程启用</dt><dd>${pin.enabled === false ? "否" : "是"}</dd></div></dl>
|
||
<section><h3>判定依据</h3><ul>${(pin.evidence || ["等待诊断证据"]).map((item) => `<li>${esc(item)}</li>`).join("")}</ul></section>
|
||
<section><h3>建议检查顺序</h3><ol>${(pin.actions || ["启动硬件诊断后生成建议"]).map((item) => `<li>${esc(item)}</li>`).join("")}</ol></section>`;
|
||
}
|
||
|
||
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", "ZCANPRO桥接", 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) => `<div data-state="${item[3]}"><small>${item[0]} · ${item[1]}</small><strong>${item[2]}</strong></div>`).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) => `<button type="button" data-state="${esc(group.state)}"><i></i><div><strong>${esc(group.name)}</strong><small>${esc(group.summary)}</small></div><span>${esc(STATE_TEXT[group.state] || group.state)}</span></button>`).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
|
||
? '<i data-lucide="square"></i>停止硬件诊断'
|
||
: '<i data-lucide="activity"></i>启动硬件诊断';
|
||
renderConnectors(hardware);
|
||
renderPinDetail(hardware);
|
||
const events = session.events || [];
|
||
$("#repairHardwareRecordsBody").innerHTML = `<p class="v2-repair-report-path">${esc(session.report_path || "停止诊断后自动保存硬件报告")}</p>${events.length ? `<table><thead><tr><th>时间</th><th>支路</th><th>状态</th><th>摘要</th></tr></thead><tbody>${[...events].reverse().map((event) => `<tr><td>${esc(event.time)}</td><td>${esc(event.name)}</td><td>${esc(STATE_TEXT[event.state] || event.state)}</td><td>${esc(event.summary)}</td></tr>`).join("")}</tbody></table>` : "<p>尚无硬件诊断事件。</p>"}`;
|
||
}
|
||
|
||
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 || {};
|
||
$("#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
|
||
: [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.zcanpro_configured;
|
||
if (executionMode === "bench" && !integration.zcanpro_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;
|
||
window.CognitiveAgentConsoleV2?.enhanceSelect($("#repairExecutionMode"));
|
||
}
|
||
|
||
function renderRecords() {
|
||
const results = latest.batch?.results || [];
|
||
const events = latest.healing?.events || [];
|
||
$("#repairSoftwareRecordsBody").innerHTML = `<p class="v2-repair-report-path">${esc(latest.latest_report_path || "成功用例将自动保存诊断快照")}</p>${results.length ? `<table><thead><tr><th>用例</th><th>结果</th><th>耗时</th><th>诊断码</th></tr></thead><tbody>${results.map((item) => `<tr><td>${esc(item.title)}</td><td>${esc(item.result)}</td><td>${Number(item.duration_s || 0).toFixed(2)} s</td><td>${esc(item.diagnosed_code || "—")}</td></tr>`).join("")}</tbody></table>` : "<p>尚无批量验证记录。</p>"}<div class="v2-repair-event-list">${events.map((event) => `<article><strong>${esc(event.title)}</strong><span>${esc(event.detail)}</span></article>`).join("")}</div>`;
|
||
}
|
||
|
||
function renderAll() {
|
||
renderGlobal();
|
||
renderCases();
|
||
renderTimeline();
|
||
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();
|
||
});
|
||
$("#repairStart").addEventListener("click", async () => {
|
||
if (executionMode === "bench" && !(await confirmAction("即将向实物 ECU 写入受控故障并驱动座椅恢复。请确认运动区域无人、无遮挡且急停可用。"))) return;
|
||
runAction(() => apiPost("/healing/start", { case_id: selectedCaseId, execution_mode: executionMode }), executionMode === "bench" ? "实物自修复用例已启动" : "影子故障已注入,开始自修复");
|
||
});
|
||
$("#repairRunAll").addEventListener("click", async () => {
|
||
if (executionMode === "bench" && !(await confirmAction(
|
||
"将依次执行6个实物故障用例;每项开始前会自动停止输出并完成四轴归零。请确认运动区域无人、无遮挡且急停可用。",
|
||
"确认实物批量验证",
|
||
))) return;
|
||
runAction(() => apiPost("/healing/run-all", { execution_mode: executionMode }), "六项自动验证已启动");
|
||
});
|
||
$("#repairReset").addEventListener("click", async () => {
|
||
if (executionMode === "bench" && !(await confirmAction("四轴将依次回到机械零位。请确认运动区域无人、无遮挡且急停可用。", "确认归零"))) return;
|
||
runAction(() => apiPost("/healing/reset", { execution_mode: executionMode }), executionMode === "bench" ? "四轴归零请求已提交" : "自修复验证已重置");
|
||
});
|
||
$("#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-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);
|
||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||
if (isActive()) poll();
|
||
window.setInterval(poll, 600);
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", init);
|
||
}());
|