feat: harden seat intent and expand HIL coverage

This commit is contained in:
vee
2026-08-02 05:20:38 +08:00
parent 4aded5c21d
commit 55d0d400cb
62 changed files with 53112 additions and 1748 deletions
+112 -25
View File
@@ -22,10 +22,22 @@ const state = {
seatActuatorState: {},
seatFeedbackSource: "simulated",
availableSeatAxes: null,
connectionStatus: window.CognitiveAgentNetwork?.getState?.().status || "connecting",
};
const $ = (selector, root = document) => root.querySelector(selector);
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
const apiRequest = (input, options) => window.CognitiveAgentNetwork.request(input, options);
// A four-axis hardware mode is executed serially. Each absolute move may wait
// up to 33 seconds, and firmware recovery or known-range homing can extend the
// request further. Keep the browser deadline above the backend's bounded HIL
// workflow so the UI does not report failure while the seat is still moving.
const HARDWARE_REQUEST_TIMEOUT_MS = 15 * 60 * 1000;
const REQUEST_TIMEOUT_MS = 45 * 1000;
function requestTimeoutForExecutionMode(mode) {
return mode === "hardware" ? HARDWARE_REQUEST_TIMEOUT_MS : REQUEST_TIMEOUT_MS;
}
function refreshIcons() {
if (window.lucide) {
@@ -2519,6 +2531,10 @@ function updateSeatFeedbackModeLabel() {
label.textContent = "PHYSICAL CAN · SYNC UNAVAILABLE";
return;
}
if (state.seatFeedbackSource === "simulated_hardware") {
label.textContent = "SIMULATED HIL · NO PHYSICAL CAN";
return;
}
label.textContent = state.executionMode === "hardware"
? "PHYSICAL CAN FEEDBACK"
: "SIMULATED FEEDBACK";
@@ -2526,30 +2542,41 @@ function updateSeatFeedbackModeLabel() {
function selectExecutionMode(mode) {
if (mode === "hardware" && !state.runtime?.hardware?.configured) return;
if (mode === "simulated_hardware" && !state.runtime?.simulated_hardware?.enabled) return;
state.executionMode = mode;
$$('[data-execution-mode]').forEach((button) => {
button.classList.toggle("active", button.dataset.executionMode === mode);
});
const hardware = mode === "hardware";
const simulatedHardware = mode === "simulated_hardware";
const approvalRequired = Boolean(state.runtime?.hardware?.approval_required);
$("#approvalField").hidden = !hardware || !approvalRequired;
$("#executionBadge").className = `status-badge ${hardware ? "" : "neutral"}`.trim();
$("#simulationFaultField").hidden = !simulatedHardware;
$("#executionBadge").className = `status-badge ${hardware ? "" : simulatedHardware ? "warning" : "neutral"}`.trim();
$("#executionBadge").innerHTML = hardware
? '<i data-lucide="radio-tower"></i>HIL · 真实 CAN'
: '<i data-lucide="flask-conical"></i>DRY-RUN';
$("#safetyTitle").textContent = hardware ? "真实台架安全门禁" : "虚拟执行边界";
: simulatedHardware
? '<i data-lucide="bug-play"></i>SIM · 模拟台架'
: '<i data-lucide="flask-conical"></i>DRY-RUN';
$("#safetyTitle").textContent = hardware
? "真实台架安全门禁"
: simulatedHardware ? "模拟台架隔离边界" : "虚拟执行边界";
$("#safetyText").textContent = hardware
? "副驾座椅映射到统一实体执行器;绝对位置会实时检查板端标定,小幅相对移动可走受限定时/Hall 回读,异常会尝试 STOP_ALL。"
: "四域 CAN 均为 dry-run,真实发送被强制关闭。";
$("#executeStageLabel").textContent = hardware ? "台架执行" : "虚拟执行";
: simulatedHardware
? "故障只注入执行报告;物理 CAN、ZLG 适配器和 IAR 固件路径保持关闭。"
: "四域 CAN 均为 dry-run,真实发送被强制关闭。";
$("#executeStageLabel").textContent = hardware ? "台架执行" : simulatedHardware ? "故障仿真" : "虚拟执行";
$("#feedbackModeLabel").textContent = hardware
? "T2G BENCH · PHYSICAL CAN FEEDBACK"
: "MATRIX-BACKED · SIMULATED FEEDBACK";
: simulatedHardware
? "SIMULATED HIL · FAULT INJECTION"
: "MATRIX-BACKED · SIMULATED FEEDBACK";
const runButtonLabel = $("#runButton span");
if (runButtonLabel) {
runButtonLabel.textContent = hardware
? (approvalRequired ? "授权并真实执行" : "真实执行")
: "生成并执行";
: simulatedHardware ? "运行故障仿真" : "生成并执行";
}
updateSeatFeedbackModeLabel();
refreshIcons();
@@ -2730,7 +2757,7 @@ function saveCurrentModeProfile() {
async function loadRuntime() {
try {
const response = await fetch("/api/runtime", { cache: "no-store" });
const response = await apiRequest("/api/runtime", { cache: "no-store" });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
state.runtime = await response.json();
populateVehicleSelector();
@@ -2741,7 +2768,8 @@ async function loadRuntime() {
if (state.runtime.openai_available) {
badge.classList.remove("danger");
badge.innerHTML = `<i data-lucide="sparkles"></i>${state.runtime.openai_model}`;
$("#providerNote").textContent = `${state.runtime.openai_model} · Responses API · 失败时自动降级本地规则`;
const responsesEndpoint = state.runtime.openai_responses_endpoint || "Responses API";
$("#providerNote").textContent = `${state.runtime.openai_model} · ${responsesEndpoint} · 失败时自动降级本地规则`;
selectProvider("openai");
} else {
badge.classList.add("danger");
@@ -2754,6 +2782,11 @@ async function loadRuntime() {
$("#matrixCount").textContent = `${state.runtime.matrix.messages} / ${state.runtime.matrix.signals}`;
const hardwareButton = $("#hardwareMode");
hardwareButton.disabled = !hardware.configured;
const simulation = state.runtime.simulated_hardware || {};
$("#simulationMode").disabled = !simulation.enabled;
$("#simulationMode").title = simulation.enabled
? "开发专用模拟台架,不连接物理 CAN"
: "通过安卓联调脚本或环境变量启用模拟台架";
const profileText = (hardware.firmware_profiles || []).join(" / ") || "未选择 profile";
const flashText = hardware.auto_flash_enabled ? "自动烧录已启用" : "自动烧录未启用";
const approvalText = hardware.approval_required ? "需授权令牌" : "调试免令牌";
@@ -2772,7 +2805,12 @@ async function loadRuntime() {
? `可选 · ${approvalText}`
: "已阻断";
$("#hardwareCoverage").className = hardware.configured ? "available" : "blocked";
selectExecutionMode("dry_run");
const modeAvailable = state.executionMode === "hardware"
? hardware.configured
: state.executionMode === "simulated_hardware"
? simulation.enabled
: true;
selectExecutionMode(modeAvailable ? state.executionMode : "dry_run");
refreshIcons();
} catch (error) {
$("#modelBadge").textContent = "运行时离线";
@@ -2789,7 +2827,7 @@ async function synchronizePhysicalSeatState() {
vehicle_id: state.vehicleId || state.runtime.default_vehicle_id || "",
});
try {
const response = await fetch(`/api/hardware/seat-state?${query}`, {
const response = await apiRequest(`/api/hardware/seat-state?${query}`, {
cache: "no-store",
});
if (response.status === 401) {
@@ -2814,7 +2852,7 @@ async function synchronizePhysicalSeatState() {
}
async function loadAuthStatus() {
const response = await fetch("/api/auth/status", { cache: "no-store" });
const response = await apiRequest("/api/auth/status", { cache: "no-store" });
if (!response.ok) return;
const auth = await response.json();
if (auth.enabled && !auth.authenticated) {
@@ -2825,7 +2863,7 @@ async function loadAuthStatus() {
}
async function logout() {
await fetch("/api/auth/logout", { method: "POST" });
await apiRequest("/api/auth/logout", { method: "POST" });
window.location.replace("/login");
}
@@ -3423,7 +3461,10 @@ function renderCode(result) {
const feedback = telemetry?.feedback_frame
? `${telemetry.feedback_frame.frame_id} · ${telemetry.feedback_frame.data_hex}`
: "NO DIRECT FEEDBACK";
const mode = result.execution_report?.spec?.mode === "hil" ? "HIL · PHYSICAL" : "DRY-RUN";
const simulatedHil = result.execution_report?.metadata?.labels?.seat_feedback === "simulated_hardware";
const mode = simulatedHil
? "HIL · SIMULATED"
: result.execution_report?.spec?.mode === "hil" ? "HIL · PHYSICAL" : "DRY-RUN";
summary.innerHTML = `<span>Frames</span><b>${frames.length}</b><span>First payload</span><code>${frames[0].frame_id} · ${frames[0].data_hex}</code><span>Feedback</span><code>${feedback}</code><span>Mode</span><b>${mode}</b>`;
$("#planNavMeta").textContent = `${frames.length} 帧 · ${mode}`;
} else if (telemetry?.operation === "save_posture_preset") {
@@ -3459,7 +3500,8 @@ function renderExecution(result) {
summary.className = `execution-summary ${reportSucceeded ? "succeeded" : "failed"}`;
const presetSave = Boolean(result.preset_config);
const modeSave = Boolean(result.mode_profile);
const physical = report.spec.mode === "hil";
const simulatedHil = report.metadata?.labels?.seat_feedback === "simulated_hardware";
const physical = report.spec.mode === "hil" && !simulatedHil;
const executedDomains = [...new Set(report.spec.results.map((item) => item.domain))];
const motionCount = report.spec.results.filter((item) => item.telemetry?.motion_axis).length;
const firmwareResult = report.spec.results.find((item) => item.telemetry?.firmware_verified);
@@ -3468,6 +3510,8 @@ function renderExecution(result) {
? `长期模式 ${result.mode_profile.name} 已写入配置库,未生成 CAN 命令`
: presetSave
? "当前五轴姿态已持久化到会话预设仓库,未生成 CAN 命令"
: simulatedHil
? `${executedDomains.join(" / ")} 已完成隔离 HIL 仿真,未访问物理 CAN`
: physical
? `${executedDomains.join(" / ")} 已下发真实 CAN,并以 IAR 固件实际反馈确认 ${operationResults.filter((item) => item.status === "succeeded").length} 个步骤`
: `${executedDomains.join(" / ")} 共 ${report.spec.results.length} 个操作已完成 CAN 编码与虚拟执行`;
@@ -3480,7 +3524,9 @@ function renderExecution(result) {
if (!reportSucceeded) {
const failure = report.spec.results.find((item) => item.status === "failed");
const failureTelemetry = failure?.telemetry || {};
executionDetail = failureTelemetry.error_type === "CanGatewayError"
executionDetail = simulatedHil
? `模拟故障 ${report.metadata?.labels?.fault_scenario || "unknown"}:${failure?.message || "故障已注入"};物理 CAN 未打开。`
: failureTelemetry.error_type === "CanGatewayError"
? `CAN 适配器或总线访问失败:${failure?.message || "无法打开或访问 CAN 适配器。"}`
: failureTelemetry.error_type === "FirmwareProgrammingError"
? `固件协议签名验证或自动烧录失败,座椅控制帧未发送:${failure?.message || "请检查 CMSIS-DAP 探针和 IAR 配置。"}`
@@ -3496,9 +3542,15 @@ function renderExecution(result) {
? "真实台架未返回满足目标的反馈,移动链路已尝试发送 STOP_ALL。"
: failure?.message || "真实台架未返回满足目标的物理反馈。";
}
summary.innerHTML = `<i data-lucide="${reportSucceeded ? ((presetSave || modeSave) ? "bookmark-check" : "circle-check-big") : "circle-x"}"></i><div><strong>${reportSucceeded ? (modeSave ? "长期模式已保存" : presetSave ? "姿态预设已保存" : (physical ? "真实台架执行成功" : "虚拟执行成功")) : "真实台架执行失败"}</strong><span>${executionDetail}</span></div>`;
const executionTitle = reportSucceeded
? modeSave ? "长期模式已保存"
: presetSave ? "姿态预设已保存"
: simulatedHil ? "模拟台架执行成功"
: physical ? "真实台架执行成功" : "虚拟执行成功"
: simulatedHil ? "模拟故障已触发" : physical ? "真实台架执行失败" : "虚拟执行失败";
summary.innerHTML = `<i data-lucide="${reportSucceeded ? ((presetSave || modeSave) ? "bookmark-check" : "circle-check-big") : "circle-x"}"></i><div><strong>${executionTitle}</strong><span>${executionDetail}</span></div>`;
$("#resultNavMeta").textContent = reportSucceeded
? `${report.spec.results.length} 项完成 · ${physical ? "HIL" : "DRY-RUN"}`
? `${report.spec.results.length} 项完成 · ${simulatedHil ? "SIM-HIL" : physical ? "HIL" : "DRY-RUN"}`
: "执行失败 · 查看回读";
$("#resultPanelState").textContent = reportSucceeded ? "SUCCEEDED" : "FAILED";
$("#resultPanelState").className = `workspace-panel-state ${reportSucceeded ? "succeeded" : "failed"}`;
@@ -3513,10 +3565,19 @@ function renderExecution(result) {
detail.textContent = item.message;
text.append(title, detail);
const frame = document.createElement("code");
const firmwareTelemetry = item.telemetry?.firmware_verified
? [
item.telemetry.programmed_this_request ? "FLASHED" : "VERIFIED",
item.telemetry.firmware_revision == null ? null : `REV ${item.telemetry.firmware_revision}`,
item.telemetry.identity_frame_id && item.telemetry.identity_data_hex
? `${item.telemetry.identity_frame_id} · ${item.telemetry.identity_data_hex}`
: item.telemetry.protocol_signature,
].filter(Boolean).join(" · ")
: null;
frame.textContent = item.telemetry?.command_frame
? `${item.telemetry.command_frame.frame_id} ${item.telemetry.command_frame.data_hex}`
: item.telemetry?.firmware_verified
? `${item.telemetry.programmed_this_request ? "FLASHED" : "VERIFIED"} · ${item.telemetry.identity_frame_id} · ${item.telemetry.identity_data_hex}`
: firmwareTelemetry
? firmwareTelemetry
: "CONTROL PLANE · NO CAN";
row.append(text, frame);
list.append(row);
@@ -3527,7 +3588,10 @@ function renderExecution(result) {
function renderResult(result) {
state.result = result;
const reportMode = result.execution_report?.spec?.mode;
if (reportMode === "hil") {
const feedbackSource = result.execution_report?.metadata?.labels?.seat_feedback;
if (feedbackSource === "simulated_hardware") {
state.seatFeedbackSource = "simulated_hardware";
} else if (reportMode === "hil") {
state.seatFeedbackSource = result.execution_report?.spec?.physical_state?.complete
? "physical_can"
: "physical_partial";
@@ -3597,8 +3661,9 @@ async function submitRequest(event) {
if (approvalTokenInput) approvalTokenInput.value = "";
try {
const response = await fetch("/api/requests", {
const response = await apiRequest("/api/requests", {
method: "POST",
timeoutMs: requestTimeoutForExecutionMode(state.executionMode),
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
utterance,
@@ -3606,6 +3671,9 @@ async function submitRequest(event) {
conversation_id: state.conversationId,
vehicle_id: state.vehicleId || state.runtime?.default_vehicle_id,
execution_mode: state.executionMode,
...(state.executionMode === "simulated_hardware"
? { simulation_fault: $("#simulationFault").value }
: {}),
...(approvalRequired
? { approval_token: approvalToken }
: {}),
@@ -3622,9 +3690,14 @@ async function submitRequest(event) {
await synchronizePhysicalSeatState();
}
const completed = ["succeeded", "partially_succeeded"].includes(payload.status);
const simulated = payload.execution_report?.metadata?.labels?.seat_feedback === "simulated_hardware";
showToast(completed
? (payload.execution_report?.spec?.mode === "hil" ? "真实台架执行与反馈确认已完成" : "可执行动作的 dry-run 已完成")
: "请求未执行");
? (simulated
? "模拟台架执行完成"
: payload.execution_report?.spec?.mode === "hil"
? "真实台架执行与反馈确认已完成"
: "可执行动作的 dry-run 已完成")
: simulated ? "模拟故障已触发,详情见执行结果" : "请求未执行");
} catch (error) {
window.clearInterval(state.progressTimer);
resetPipeline();
@@ -3637,7 +3710,10 @@ async function submitRequest(event) {
const hardwareLabel = state.runtime?.hardware?.approval_required
? "授权并真实执行"
: "真实执行";
button.innerHTML = `<i data-lucide="play"></i><span>${state.executionMode === "hardware" ? hardwareLabel : "生成并执行"}</span>`;
const buttonLabel = state.executionMode === "hardware"
? hardwareLabel
: state.executionMode === "simulated_hardware" ? "运行故障仿真" : "生成并执行";
button.innerHTML = `<i data-lucide="play"></i><span>${buttonLabel}</span>`;
refreshIcons();
}
}
@@ -3723,6 +3799,17 @@ window.CognitiveAgentConsole = {
},
};
window.addEventListener("cognitive-agent:connection", (event) => {
const previousStatus = state.connectionStatus;
state.connectionStatus = event.detail?.status || "offline";
if (["offline", "degraded"].includes(previousStatus)
&& state.connectionStatus === "online") {
Promise.all([loadAuthStatus(), loadRuntime()])
.then(() => showToast("服务连接已恢复"))
.catch(() => {});
}
});
document.addEventListener("DOMContentLoaded", async () => {
initJointTwinLayout();
initResultWorkspace();