feat: harden seat intent and expand HIL coverage
This commit is contained in:
+112
-25
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user