优化后端
This commit is contained in:
+178
-9
@@ -92,6 +92,24 @@ const REQUEST_TIMEOUT_MS = 45 * 1000;
|
||||
const DEFAULT_PHYSICAL_STATE_POLL_INTERVAL_MS = 5000;
|
||||
const AUTHORITATIVE_CH1_SOURCES = new Set(["physical_can_ch1", "wireless_mqtt_ch1"]);
|
||||
|
||||
async function apiResponseError(response) {
|
||||
let payload = null;
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch (_error) {
|
||||
// Some proxies return an empty or non-JSON error page. Preserve the HTTP
|
||||
// status even when no structured reason is available.
|
||||
}
|
||||
const status = Number(response?.status) || 0;
|
||||
const serverMessage = String(payload?.message || payload?.error || "").trim();
|
||||
const fallback = status ? `HTTP ${status}` : "请求失败";
|
||||
const error = new Error(serverMessage ? `${fallback}:${serverMessage}` : fallback);
|
||||
error.status = status;
|
||||
error.code = String(payload?.error || "").trim();
|
||||
error.serverMessage = serverMessage;
|
||||
return error;
|
||||
}
|
||||
|
||||
function requestTimeoutForExecutionMode(mode) {
|
||||
return mode === "hardware" ? HARDWARE_REQUEST_TIMEOUT_MS : REQUEST_TIMEOUT_MS;
|
||||
}
|
||||
@@ -111,6 +129,8 @@ function renderCabinLiveStatus(networkStatus = state.connectionStatus) {
|
||||
if (!indicator) return;
|
||||
const label = $("b", indicator);
|
||||
if (!label) return;
|
||||
indicator.removeAttribute("aria-label");
|
||||
indicator.removeAttribute("title");
|
||||
const summarySystem = $("#cabinSummarySystem");
|
||||
const setSummarySystem = (text, status) => {
|
||||
if (!summarySystem) return;
|
||||
@@ -144,6 +164,22 @@ function renderCabinLiveStatus(networkStatus = state.connectionStatus) {
|
||||
}
|
||||
|
||||
const snapshot = state.vehicleStateSnapshot;
|
||||
if (snapshot?.errorStatus === 403) {
|
||||
const reason = snapshot.errorReason || "当前终端没有车辆状态读取权限";
|
||||
const visibleReason = `${transportLabel} · 403:${reason}`;
|
||||
indicator.dataset.state = "offline";
|
||||
label.textContent = visibleReason;
|
||||
indicator.title = visibleReason;
|
||||
indicator.setAttribute("aria-label", visibleReason);
|
||||
const telemetryState = $("#vehicleTelemetryState");
|
||||
if (telemetryState) telemetryState.textContent = "403 拒绝";
|
||||
setSummarySystem("403 · 状态读取被拒绝", "warning");
|
||||
return;
|
||||
}
|
||||
const telemetryState = $("#vehicleTelemetryState");
|
||||
if (telemetryState?.textContent === "403 拒绝") {
|
||||
telemetryState.textContent = "只读";
|
||||
}
|
||||
const observedIds = Array.isArray(snapshot?.observedFrameIds)
|
||||
? snapshot.observedFrameIds
|
||||
: [];
|
||||
@@ -5884,19 +5920,24 @@ function installModeChipScroller() {
|
||||
let pointerStartScroll = 0;
|
||||
let dragged = false;
|
||||
let suppressClick = false;
|
||||
let pointerCaptured = false;
|
||||
group.addEventListener("pointerdown", (event) => {
|
||||
if (event.pointerType !== "mouse" || event.button !== 0 || maximumScroll() <= 1) return;
|
||||
pointerId = event.pointerId;
|
||||
pointerStartX = event.clientX;
|
||||
pointerStartScroll = group.scrollLeft;
|
||||
dragged = false;
|
||||
group.setPointerCapture?.(pointerId);
|
||||
pointerCaptured = false;
|
||||
});
|
||||
group.addEventListener("pointermove", (event) => {
|
||||
if (pointerId !== event.pointerId) return;
|
||||
const distance = event.clientX - pointerStartX;
|
||||
if (!dragged && Math.abs(distance) < 5) return;
|
||||
dragged = true;
|
||||
if (!pointerCaptured && typeof group.setPointerCapture === "function") {
|
||||
group.setPointerCapture(pointerId);
|
||||
pointerCaptured = true;
|
||||
}
|
||||
group.classList.add("is-dragging");
|
||||
group.scrollLeft = Math.max(0, Math.min(maximumScroll(), pointerStartScroll - distance));
|
||||
updateScrollState();
|
||||
@@ -5904,8 +5945,9 @@ function installModeChipScroller() {
|
||||
});
|
||||
const finishPointerDrag = (event) => {
|
||||
if (pointerId !== event.pointerId) return;
|
||||
group.releasePointerCapture?.(pointerId);
|
||||
if (pointerCaptured) group.releasePointerCapture?.(pointerId);
|
||||
pointerId = null;
|
||||
pointerCaptured = false;
|
||||
group.classList.remove("is-dragging");
|
||||
if (dragged) {
|
||||
suppressClick = true;
|
||||
@@ -6094,11 +6136,113 @@ function installModeActionPreview() {
|
||||
window.addEventListener("resize", () => hideModeActionPopover({ immediate: true }), { passive: true });
|
||||
}
|
||||
|
||||
function vueModeChipEntry(mode) {
|
||||
const timeline = mode?.timeline;
|
||||
const actionCount = modeExecutionActionViews(mode).length
|
||||
|| Number(mode?.operation_count || 0);
|
||||
const phaseSummary = Array.isArray(timeline?.phases)
|
||||
? timeline.phases.map((phase) => {
|
||||
if (Number(phase.duration_ms || 0) > 0) {
|
||||
return `${phase.name} ${modeSecondsLabel(phase.duration_ms)}s`;
|
||||
}
|
||||
const timeout = Number(phase.timeout_ms || 0);
|
||||
return timeout > 0
|
||||
? `${phase.name} 目标到位(保护上限${modeSecondsLabel(timeout)}s)`
|
||||
: `${phase.name} 目标到位`;
|
||||
}).join(" → ")
|
||||
: "";
|
||||
let cycleLabel = "";
|
||||
if (["infinite", "until_stopped_or_timeout"].includes(timeline?.repeat)) {
|
||||
const runtimeMinutes = Number(timeline.max_runtime_ms || 0) / 60000;
|
||||
const runtimeLabel = Number.isInteger(runtimeMinutes)
|
||||
? String(runtimeMinutes)
|
||||
: runtimeMinutes.toFixed(1);
|
||||
const cycleDurationMs = Number(timeline.cycle_duration_ms || 0);
|
||||
const cycle = cycleDurationMs > 0
|
||||
? `${modeSecondsLabel(cycleDurationMs)}s循环`
|
||||
: "目标到位循环";
|
||||
cycleLabel = runtimeMinutes > 0 ? `${cycle} · 最长${runtimeLabel}min` : cycle;
|
||||
}
|
||||
return {
|
||||
type: "mode",
|
||||
key: `mode-${mode.id}`,
|
||||
id: mode.id,
|
||||
label: modeChipLabel(mode),
|
||||
summary: modeChipSummary(mode),
|
||||
icon: mode.icon || "bookmark",
|
||||
command: `进入${mode.name}`,
|
||||
origin: mode?.origin || "preset",
|
||||
behavior: cycleLabel ? "cyclic" : "",
|
||||
cycleLabel,
|
||||
ariaLabel: `${mode.name || mode.id},${actionCount} 项执行动作,点击执行`,
|
||||
title: [mode.description || mode.name, phaseSummary, "悬浮或聚焦查看全部执行动作"]
|
||||
.filter(Boolean)
|
||||
.join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
function vueModeChipEntries(modes) {
|
||||
const preferredStaticOrder = [
|
||||
"zero_gravity",
|
||||
"office",
|
||||
"nap",
|
||||
"queen_passenger_driving",
|
||||
"leisure_lounger",
|
||||
];
|
||||
const userModes = modes.filter(isUserModeProfile);
|
||||
const dynamicOrder = new Map([["rocking_horse", 0], ["dynamic_cradle", 1]]);
|
||||
const dynamicModes = modes
|
||||
.filter((mode) => !isUserModeProfile(mode) && isDynamicModeProfile(mode))
|
||||
.sort((left, right) => (
|
||||
(dynamicOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER)
|
||||
- (dynamicOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER)
|
||||
));
|
||||
const staticModes = modes.filter(
|
||||
(mode) => !isUserModeProfile(mode) && !isDynamicModeProfile(mode),
|
||||
);
|
||||
const preferredStaticModes = preferredStaticOrder
|
||||
.map((modeId) => staticModes.find((mode) => mode.id === modeId))
|
||||
.filter(Boolean);
|
||||
const otherStaticModes = staticModes.filter((mode) => !preferredStaticModes.includes(mode));
|
||||
const entries = [];
|
||||
const appendLabel = (label, id) => entries.push({
|
||||
type: "label",
|
||||
key: `label-${id}`,
|
||||
id,
|
||||
label,
|
||||
});
|
||||
const appendDivider = (id) => entries.push({
|
||||
type: "divider",
|
||||
key: `divider-${id}`,
|
||||
id,
|
||||
});
|
||||
if (userModes.length) {
|
||||
appendLabel("自定义模式", "custom");
|
||||
entries.push(...userModes.map(vueModeChipEntry));
|
||||
}
|
||||
if (userModes.length && (staticModes.length || dynamicModes.length)) appendDivider("custom");
|
||||
if (staticModes.length) {
|
||||
appendLabel("静态模式", "static");
|
||||
entries.push(...preferredStaticModes.map(vueModeChipEntry));
|
||||
entries.push(...otherStaticModes.map(vueModeChipEntry));
|
||||
}
|
||||
if (dynamicModes.length) {
|
||||
if (userModes.length || staticModes.length) appendDivider("dynamic");
|
||||
appendLabel("动态模式", "dynamic");
|
||||
entries.push(...dynamicModes.map(vueModeChipEntry));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function populateModeChips() {
|
||||
const group = $(".mode-chips");
|
||||
if (!group) return;
|
||||
const buttons = () => $$("button", group);
|
||||
const modes = modeProfiles();
|
||||
if (window.CognitiveAgentVueUI?.renderModeChips?.(group, vueModeChipEntries(modes))) {
|
||||
populateModeGallery();
|
||||
return;
|
||||
}
|
||||
const runtimeModeIds = new Set(modes.map((mode) => mode.id));
|
||||
$$('button[data-mode-origin="user"]', group).forEach((button) => {
|
||||
if (!runtimeModeIds.has(button.dataset.modeProfileId)) button.remove();
|
||||
@@ -6155,6 +6299,8 @@ function populateModeChips() {
|
||||
runIcon.dataset.lucide = "play";
|
||||
button.append(runIcon);
|
||||
}
|
||||
runIcon.dataset.modeRun = "true";
|
||||
runIcon.title = `执行${mode.name || mode.id}`;
|
||||
const timeline = mode.timeline;
|
||||
let cycleBadge = $(".mode-cycle-badge", button);
|
||||
if (["infinite", "until_stopped_or_timeout"].includes(timeline?.repeat)) {
|
||||
@@ -6330,8 +6476,10 @@ function setModeChipsDisabled(disabled) {
|
||||
});
|
||||
}
|
||||
|
||||
async function executeSavedModeProfile(mode) {
|
||||
async function executeSavedModeProfile(mode, { command = "" } = {}) {
|
||||
if (!mode?.id || !mode?.command_flow?.source_request_id) return false;
|
||||
const modeCommand = String(command || `进入${mode.name || mode.id}`).trim();
|
||||
const assistant = window.CognitiveAgentAssistant;
|
||||
const driverOnlyCh1 = isDriverOnlyCh1Runtime();
|
||||
if (state.executionMode === "hardware"
|
||||
&& state.controlTransport !== "wireless_mqtt"
|
||||
@@ -6346,10 +6494,13 @@ async function executeSavedModeProfile(mode) {
|
||||
|
||||
closeModeGallery({ restoreFocus: false });
|
||||
const runButton = $("#runButton");
|
||||
const input = $("#utterance");
|
||||
runButton.disabled = true;
|
||||
setModeChipsDisabled(true);
|
||||
setModeExecutionState(mode.id, "running");
|
||||
$("#utterance").value = mode.name || mode.id;
|
||||
assistant?.open?.();
|
||||
input.value = modeCommand;
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
setRunStatus("processing", "MODE COMMAND FLOW");
|
||||
$("#resultTitle").textContent = `正在直接执行${mode.name}`;
|
||||
try {
|
||||
@@ -6375,11 +6526,11 @@ async function executeSavedModeProfile(mode) {
|
||||
if (!payload) throw new Error("模式卡片未返回执行结果");
|
||||
renderResult(payload);
|
||||
const completed = ["succeeded", "partially_succeeded"].includes(payload.status);
|
||||
$("#utterance").value = mode.name || mode.id;
|
||||
$("#resultTitle").textContent = completed
|
||||
? `${mode.name}执行完成`
|
||||
: `${mode.name}执行未完成`;
|
||||
setModeExecutionState(mode.id, completed ? "succeeded" : "failed");
|
||||
assistant?.announceModeExecution?.(mode, payload, { command: modeCommand });
|
||||
if (state.executionMode === "hardware") await synchronizePhysicalSeatState();
|
||||
showToast(completed
|
||||
? `${mode.name}已直接执行保存的控制指令流`
|
||||
@@ -6388,8 +6539,13 @@ async function executeSavedModeProfile(mode) {
|
||||
setModeExecutionState(mode.id, "failed");
|
||||
setRunStatus("failed", "MODE FAILED");
|
||||
$("#resultTitle").textContent = `${mode.name}执行失败`;
|
||||
assistant?.announceModeExecution?.(mode, null, { command: modeCommand, error });
|
||||
showToast(error.message || "模式执行失败");
|
||||
} finally {
|
||||
if (input.value === modeCommand) {
|
||||
input.value = "";
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
}
|
||||
runButton.disabled = false;
|
||||
setModeChipsDisabled(false);
|
||||
}
|
||||
@@ -6412,6 +6568,10 @@ function applyModeCommand(command, modeProfileId = null) {
|
||||
setModeRailSelection(mode);
|
||||
closeModeGallery({ restoreFocus: false });
|
||||
assistant?.open?.();
|
||||
if (mode?.command_flow?.source_request_id) {
|
||||
void executeSavedModeProfile(mode, { command: modeCommand });
|
||||
return true;
|
||||
}
|
||||
const input = $("#utterance");
|
||||
const form = $("#requestForm");
|
||||
if (!input || !form) {
|
||||
@@ -6468,7 +6628,7 @@ function applyRuntimeTargetPolicy(runtime) {
|
||||
async function loadRuntime() {
|
||||
try {
|
||||
const response = await apiRequest("/api/runtime", { cache: "no-store" });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
if (!response.ok) throw await apiResponseError(response);
|
||||
state.runtime = await response.json();
|
||||
renderControlTransport();
|
||||
applyRuntimeTargetPolicy(state.runtime);
|
||||
@@ -6487,6 +6647,7 @@ async function loadRuntime() {
|
||||
mqttDualChannel ? vehicleCan.configured : hardware.configured,
|
||||
);
|
||||
const vehicleHardwareReady = Boolean(vehicleCan.configured);
|
||||
const linkedHardwareReady = seatHardwareReady && vehicleHardwareReady;
|
||||
const ch1DriverOnly = vehicleHardwareReady && !seatHardwareReady;
|
||||
const hardwareExecutionReady = Boolean(hardware.configured || vehicleCan.configured);
|
||||
const gptButton = $('[data-provider="openai"]');
|
||||
@@ -6563,7 +6724,7 @@ async function synchronizePassengerSeatState() {
|
||||
window.location.replace("/login");
|
||||
return false;
|
||||
}
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
if (!response.ok) throw await apiResponseError(response);
|
||||
const payload = await response.json();
|
||||
if (requestedTransport !== state.controlTransport) return false;
|
||||
updateWiredCanProbe("passenger", true, requestedTransport);
|
||||
@@ -6605,7 +6766,7 @@ async function synchronizeDriverSeatState() {
|
||||
window.location.replace("/login");
|
||||
return false;
|
||||
}
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
if (!response.ok) throw await apiResponseError(response);
|
||||
const payload = await response.json();
|
||||
if (requestedTransport !== state.controlTransport) return false;
|
||||
updateWiredCanProbe("vehicle", true, requestedTransport);
|
||||
@@ -6627,6 +6788,9 @@ async function synchronizeDriverSeatState() {
|
||||
state.vehicleStateSnapshot = {
|
||||
...(state.vehicleStateSnapshot || {}),
|
||||
error: error.message || String(error),
|
||||
errorStatus: Number(error.status) || null,
|
||||
errorCode: error.code || "",
|
||||
errorReason: error.serverMessage || error.message || String(error),
|
||||
};
|
||||
renderCabinLiveStatus();
|
||||
if (!["physical_can_ch1", "wireless_mqtt_ch1"].includes(state.seatFeedbackSources.driver)) {
|
||||
@@ -7950,6 +8114,10 @@ function recordVehicleStateTelemetry(payload) {
|
||||
: "physical_can_ch1";
|
||||
state.vehicleStateSnapshot = {
|
||||
...previous,
|
||||
error: null,
|
||||
errorStatus: null,
|
||||
errorCode: "",
|
||||
errorReason: "",
|
||||
source,
|
||||
capturedAt: payload.captured_at || null,
|
||||
receivedAt: Date.now(),
|
||||
@@ -8716,7 +8884,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
installModeActionPreview();
|
||||
$(".mode-chips").addEventListener("click", (event) => {
|
||||
const target = event.target instanceof Element ? event.target : null;
|
||||
const button = target?.closest("button[data-mode-command]");
|
||||
const runControl = target?.closest("[data-mode-run]");
|
||||
const button = (runControl || target)?.closest("button[data-mode-command]");
|
||||
if (button) applyModeCommand(
|
||||
button.dataset.modeCommand,
|
||||
button.dataset.modeProfileId || null,
|
||||
|
||||
Reference in New Issue
Block a user