feat(n7web): sync N7 scene cards and optimize runtime
This commit is contained in:
+324
-28
@@ -60,6 +60,8 @@ const state = {
|
||||
vehicleStateTurn: -1,
|
||||
seatTwin: null,
|
||||
seatActuatorState: {},
|
||||
seatRenderFingerprint: "",
|
||||
domainRenderFingerprint: "",
|
||||
seatFeedbackSource: "simulated",
|
||||
seatFeedbackSources: { passenger: "simulated", driver: "unavailable" },
|
||||
availableSeatAxes: null,
|
||||
@@ -76,6 +78,14 @@ const state = {
|
||||
modeRunTimer: null,
|
||||
modeHighlightTimer: null,
|
||||
latestModeRefreshKey: "",
|
||||
modeProfileSync: {
|
||||
initialized: false,
|
||||
inFlight: false,
|
||||
poller: null,
|
||||
catalogRevision: "",
|
||||
importedCount: 0,
|
||||
rejectedCount: 0,
|
||||
},
|
||||
modeEditor: null,
|
||||
directCanCommandPending: false,
|
||||
};
|
||||
@@ -90,6 +100,7 @@ const apiRequest = (input, options) => window.CognitiveAgentNetwork.request(inpu
|
||||
const HARDWARE_REQUEST_TIMEOUT_MS = 15 * 60 * 1000;
|
||||
const REQUEST_TIMEOUT_MS = 45 * 1000;
|
||||
const DEFAULT_PHYSICAL_STATE_POLL_INTERVAL_MS = 5000;
|
||||
const MODE_PROFILE_SYNC_POLL_INTERVAL_MS = 1000;
|
||||
const AUTHORITATIVE_CH1_SOURCES = new Set(["physical_can_ch1", "wireless_mqtt_ch1"]);
|
||||
|
||||
async function apiResponseError(response) {
|
||||
@@ -2025,6 +2036,7 @@ function applyTwinSceneTheme(twin) {
|
||||
canvas.dataset.themeRole = role;
|
||||
canvas.dataset.themeAccent = `#${palette.accent.toString(16).padStart(6, "0")}`;
|
||||
}
|
||||
twin.requestRender?.({ hotspots: false });
|
||||
}
|
||||
|
||||
function updateTwinVehicleVisual(systems, timestamp) {
|
||||
@@ -2574,6 +2586,10 @@ function configureFixedTwinSeat(THREE, model, position, role) {
|
||||
return model;
|
||||
}
|
||||
|
||||
function requestSeatTwinRender(options = {}) {
|
||||
state.seatTwin?.requestRender?.(options);
|
||||
}
|
||||
|
||||
function setTwinSelection(selected) {
|
||||
const twin = state.seatTwin;
|
||||
if (!twin) return;
|
||||
@@ -2609,6 +2625,7 @@ function setTwinSelection(selected) {
|
||||
twin.view.targetX = 0;
|
||||
twin.view.yaw = -0.62;
|
||||
renderTwinReadout();
|
||||
requestSeatTwinRender();
|
||||
}
|
||||
|
||||
function twinDeviceForSeat(seat, actuatorState = state.seatTwin?.actuatorState || {}) {
|
||||
@@ -2734,6 +2751,7 @@ function updateSeatTwin(actuatorState) {
|
||||
});
|
||||
});
|
||||
renderTwinReadout();
|
||||
requestSeatTwinRender();
|
||||
}
|
||||
|
||||
const REFERENCE_VEHICLE_ROOT = "/assets/models/byd-tang/";
|
||||
@@ -4080,7 +4098,7 @@ function initSeatTwin() {
|
||||
return;
|
||||
}
|
||||
renderer.autoClear = false;
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.5));
|
||||
if ("outputColorSpace" in renderer) renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
else renderer.outputEncoding = THREE.sRGBEncoding;
|
||||
renderer.shadowMap.enabled = true;
|
||||
@@ -4218,6 +4236,15 @@ function initSeatTwin() {
|
||||
view: { yaw: -0.62, pitch: 0.31, distance: 10.2, targetX: 0 },
|
||||
dragging: false,
|
||||
pointer: { x: 0, y: 0 },
|
||||
animationFrame: null,
|
||||
renderRequested: false,
|
||||
settleFrames: 0,
|
||||
inViewport: true,
|
||||
hotspotsDirty: true,
|
||||
hotspotLastLayoutAt: Number.NEGATIVE_INFINITY,
|
||||
hotspotMetrics: new Map(),
|
||||
hotspotElements: new Map(),
|
||||
requestRender,
|
||||
};
|
||||
applyTwinSceneTheme(state.seatTwin);
|
||||
window.addEventListener("cognitive-agent:module-change", () => {
|
||||
@@ -4284,6 +4311,7 @@ function initSeatTwin() {
|
||||
distance: 10.2,
|
||||
targetX: 0,
|
||||
});
|
||||
requestRender();
|
||||
}
|
||||
|
||||
let stageWasVisible = false;
|
||||
@@ -4294,6 +4322,8 @@ function initSeatTwin() {
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
state.seatTwin.narrowViewport = width < 600;
|
||||
state.seatTwin.hotspotMetrics.clear();
|
||||
requestRender({ settleFrames: 2 });
|
||||
}
|
||||
|
||||
function synchronizeTwinViewport({ reset = false } = {}) {
|
||||
@@ -4317,6 +4347,31 @@ function initSeatTwin() {
|
||||
stageObserver.observe(stage);
|
||||
state.seatTwin.resizeObserver = stageObserver;
|
||||
}
|
||||
if (window.IntersectionObserver) {
|
||||
const visibilityObserver = new IntersectionObserver(([entry]) => {
|
||||
const twin = state.seatTwin;
|
||||
if (!twin) return;
|
||||
twin.inViewport = Boolean(entry?.isIntersecting);
|
||||
if (!twin.inViewport && twin.animationFrame !== null) {
|
||||
window.cancelAnimationFrame(twin.animationFrame);
|
||||
twin.animationFrame = null;
|
||||
} else if (twin.inViewport) {
|
||||
requestRender({ settleFrames: 2 });
|
||||
}
|
||||
}, { root: null, rootMargin: "100px" });
|
||||
visibilityObserver.observe(stage);
|
||||
state.seatTwin.visibilityObserver = visibilityObserver;
|
||||
}
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
const twin = state.seatTwin;
|
||||
if (!twin) return;
|
||||
if (document.visibilityState !== "visible" && twin.animationFrame !== null) {
|
||||
window.cancelAnimationFrame(twin.animationFrame);
|
||||
twin.animationFrame = null;
|
||||
return;
|
||||
}
|
||||
requestRender({ settleFrames: 2 });
|
||||
});
|
||||
window.addEventListener("resize", scheduleViewportSync);
|
||||
window.addEventListener("cognitive-agent:module-change", scheduleViewportSync);
|
||||
canvas.addEventListener("webglcontextlost", (event) => {
|
||||
@@ -4342,6 +4397,7 @@ function initSeatTwin() {
|
||||
refreshIcons();
|
||||
window.setTimeout(() => { fallback.hidden = true; }, 3200);
|
||||
}
|
||||
requestRender({ settleFrames: 8 });
|
||||
});
|
||||
synchronizeTwinViewport({ reset: true });
|
||||
stage.addEventListener("pointerdown", (event) => {
|
||||
@@ -4349,6 +4405,7 @@ function initSeatTwin() {
|
||||
state.seatTwin.pointer = { x: event.clientX, y: event.clientY };
|
||||
stage.setPointerCapture(event.pointerId);
|
||||
stage.classList.add("dragging");
|
||||
requestRender({ settleFrames: 2 });
|
||||
});
|
||||
stage.addEventListener("pointermove", (event) => {
|
||||
const twin = state.seatTwin;
|
||||
@@ -4358,11 +4415,13 @@ function initSeatTwin() {
|
||||
twin.pointer = { x: event.clientX, y: event.clientY };
|
||||
twin.view.yaw -= dx * 0.006;
|
||||
twin.view.pitch = Math.max(0.12, Math.min(0.68, twin.view.pitch + dy * 0.004));
|
||||
requestRender({ settleFrames: 2 });
|
||||
});
|
||||
const endDrag = () => {
|
||||
if (!state.seatTwin) return;
|
||||
state.seatTwin.dragging = false;
|
||||
stage.classList.remove("dragging");
|
||||
requestRender({ settleFrames: 2 });
|
||||
};
|
||||
stage.addEventListener("pointerup", endDrag);
|
||||
stage.addEventListener("pointercancel", endDrag);
|
||||
@@ -4370,6 +4429,7 @@ function initSeatTwin() {
|
||||
event.preventDefault();
|
||||
const twin = state.seatTwin;
|
||||
twin.view.distance = Math.max(9.4, Math.min(15.2, twin.view.distance + event.deltaY * 0.006));
|
||||
requestRender({ settleFrames: 2 });
|
||||
}, { passive: false });
|
||||
$("#twinReset").addEventListener("click", resetView);
|
||||
$$(".twin-seat-button").forEach((button) => button.addEventListener("click", () => setTwinSelection(button.dataset.twinSeat)));
|
||||
@@ -4392,7 +4452,11 @@ function initSeatTwin() {
|
||||
scene.updateMatrixWorld(true);
|
||||
camera.updateMatrixWorld();
|
||||
Object.entries(TWIN_DEVICE_HOTSPOT_LAYOUT).forEach(([deviceId, layout]) => {
|
||||
const button = $(`[data-twin-device="${deviceId}"]`, hotspotLayer);
|
||||
let button = twin.hotspotElements.get(deviceId);
|
||||
if (!button?.isConnected) {
|
||||
button = $(`[data-twin-device="${deviceId}"]`, hotspotLayer);
|
||||
if (button) twin.hotspotElements.set(deviceId, button);
|
||||
}
|
||||
if (!button || !twinDeviceWorldAnchor(THREE, twin, deviceId, hotspotWorldAnchor)) return;
|
||||
projectedHotspot.copy(hotspotWorldAnchor).project(camera);
|
||||
const anchorX = (projectedHotspot.x * 0.5 + 0.5) * width;
|
||||
@@ -4412,8 +4476,18 @@ function initSeatTwin() {
|
||||
button.dataset.anchorSource = "projected-3d";
|
||||
|
||||
const label = $(".v2-twin-hotspot-label", button);
|
||||
const halfWidth = Math.max(44, (label?.offsetWidth || 94) / 2);
|
||||
const halfHeight = Math.max(20, (label?.offsetHeight || 42) / 2);
|
||||
let metrics = twin.hotspotMetrics.get(deviceId);
|
||||
if (!metrics || metrics.label !== label) {
|
||||
const measuredWidth = label?.offsetWidth || 94;
|
||||
const measuredHeight = label?.offsetHeight || 42;
|
||||
metrics = {
|
||||
label,
|
||||
halfWidth: Math.max(44, measuredWidth / 2),
|
||||
halfHeight: Math.max(20, measuredHeight / 2),
|
||||
};
|
||||
twin.hotspotMetrics.set(deviceId, metrics);
|
||||
}
|
||||
const { halfWidth, halfHeight } = metrics;
|
||||
const preferredX = anchorX + layout.labelOffset[0] * offsetScale;
|
||||
const preferredY = anchorY + layout.labelOffset[1] * offsetScale;
|
||||
const labelX = clampLabelCoordinate(
|
||||
@@ -4448,22 +4522,84 @@ function initSeatTwin() {
|
||||
window.CognitiveAgentConsoleV2?.positionTwinDevicePopover?.();
|
||||
}
|
||||
|
||||
function queueTwinFrame() {
|
||||
const twin = state.seatTwin;
|
||||
if (!twin || twin.animationFrame !== null) return;
|
||||
if (document.visibilityState !== "visible" || twin.inViewport === false) return;
|
||||
twin.animationFrame = window.requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
function requestRender({ hotspots = true, settleFrames = 36 } = {}) {
|
||||
const twin = state.seatTwin;
|
||||
if (!twin) return;
|
||||
twin.renderRequested = true;
|
||||
twin.settleFrames = Math.max(twin.settleFrames || 0, Math.max(0, Number(settleFrames) || 0));
|
||||
if (hotspots) twin.hotspotsDirty = true;
|
||||
queueTwinFrame();
|
||||
}
|
||||
|
||||
function twinNeedsAnimation(twin) {
|
||||
if (twin.dragging || twin.renderRequested || twin.hotspotsDirty || twin.settleFrames > 0) {
|
||||
return true;
|
||||
}
|
||||
const movingSeat = Object.values(twin.models).some((model) => (
|
||||
Object.keys(model.positions).some(
|
||||
(axis) => Math.abs(model.targets[axis] - model.positions[axis]) >= 0.002,
|
||||
)
|
||||
|| Object.keys(model.angles).some(
|
||||
(axis) => Math.abs(model.angleTargets[axis] - model.angles[axis]) >= 0.002,
|
||||
)
|
||||
|| ["ventilation", "heating"].some((feature) => (
|
||||
Math.abs(model.thermalTargets[feature] - model.thermalLevels[feature]) >= 0.002
|
||||
|| model.thermalTargets[feature] > 0.015
|
||||
|| model.thermalLevels[feature] > 0.015
|
||||
))
|
||||
));
|
||||
if (movingSeat) return true;
|
||||
const systems = twin.vehicleSystems;
|
||||
if (!systems) return false;
|
||||
const desiredAmbient = systems.targets.ambientPower ? systems.targets.ambientBrightness : 0;
|
||||
const movingSystem = [
|
||||
[systems.current.ambientBrightness, desiredAmbient],
|
||||
[systems.current.climateLevel, systems.targets.climateLevel],
|
||||
[systems.current.roofTransmittance, systems.targets.roofTransmittance],
|
||||
[systems.current.roofShadePosition, systems.targets.roofShadePosition],
|
||||
[systems.current.lowBeam, systems.targets.lowBeam],
|
||||
[systems.current.highBeam, systems.targets.highBeam],
|
||||
[systems.current.fogLight, systems.targets.fogLight],
|
||||
[systems.current.turnSignal, systems.targets.turnSignal],
|
||||
...Object.keys(systems.current.windowPositions).map((key) => [
|
||||
systems.current.windowPositions[key],
|
||||
systems.targets.windowPositions[key],
|
||||
]),
|
||||
].some(([current, target]) => Math.abs(Number(target) - Number(current)) >= 0.002);
|
||||
return movingSystem
|
||||
|| systems.targets.turnSignal > 0.015
|
||||
|| systems.targets.climateLevel > 0.015
|
||||
|| systems.airflow.level > 0.015;
|
||||
}
|
||||
|
||||
function animate(timestamp = 0) {
|
||||
const twin = state.seatTwin;
|
||||
if (!twin) return;
|
||||
twin.animationFrame = null;
|
||||
if (document.visibilityState !== "visible" || twin.inViewport === false) return;
|
||||
if (!stageHasLayout() || canvas.dataset.contextLost === "true") {
|
||||
stageWasVisible = false;
|
||||
window.requestAnimationFrame(animate);
|
||||
twin.renderRequested = true;
|
||||
return;
|
||||
}
|
||||
if (!stageWasVisible) synchronizeTwinViewport({ reset: true });
|
||||
let geometryChanged = false;
|
||||
Object.values(twin.models).forEach((model) => {
|
||||
Object.keys(model.positions).forEach((axis) => {
|
||||
const delta = model.targets[axis] - model.positions[axis];
|
||||
if (Math.abs(delta) >= 0.002) geometryChanged = true;
|
||||
model.positions[axis] += Math.abs(delta) < 0.03 ? delta : delta * 0.085;
|
||||
});
|
||||
Object.keys(model.angles).forEach((axis) => {
|
||||
const delta = model.angleTargets[axis] - model.angles[axis];
|
||||
if (Math.abs(delta) >= 0.002) geometryChanged = true;
|
||||
model.angles[axis] += Math.abs(delta) < 0.03 ? delta : delta * 0.085;
|
||||
});
|
||||
model.slide.position.z = (model.positions.longitudinal - 50) * 0.012;
|
||||
@@ -4499,6 +4635,7 @@ function initSeatTwin() {
|
||||
);
|
||||
updateTwinThermalVisual(model, timestamp);
|
||||
});
|
||||
if (geometryChanged) twin.hotspotsDirty = true;
|
||||
updateTwinVehicleVisual(twin.vehicleSystems, timestamp);
|
||||
const { view } = twin;
|
||||
// Fit the whole reference vehicle against the limiting horizontal field
|
||||
@@ -4519,7 +4656,11 @@ function initSeatTwin() {
|
||||
);
|
||||
camera.lookAt(cameraTargetX, 1.14, -0.08);
|
||||
updateReferenceWindowOutlineLayers(twin.vehicleSystems, camera);
|
||||
positionTwinDeviceHotspots();
|
||||
if (twin.hotspotsDirty && timestamp - twin.hotspotLastLayoutAt >= 100) {
|
||||
positionTwinDeviceHotspots();
|
||||
twin.hotspotsDirty = false;
|
||||
twin.hotspotLastLayoutAt = timestamp;
|
||||
}
|
||||
canvas.dataset.framing = "whole-vehicle-centered";
|
||||
renderer.clear();
|
||||
camera.layers.set(0);
|
||||
@@ -4532,10 +4673,12 @@ function initSeatTwin() {
|
||||
scene.background = sceneBackground;
|
||||
camera.layers.set(0);
|
||||
if (!canvas.dataset.rendered) canvas.dataset.rendered = "true";
|
||||
window.requestAnimationFrame(animate);
|
||||
twin.renderRequested = false;
|
||||
twin.settleFrames = Math.max(0, twin.settleFrames - 1);
|
||||
if (twinNeedsAnimation(twin)) queueTwinFrame();
|
||||
}
|
||||
setTwinSelection("passenger");
|
||||
animate();
|
||||
requestRender({ settleFrames: 48 });
|
||||
}
|
||||
|
||||
function showToast(message) {
|
||||
@@ -5114,6 +5257,10 @@ function isUserModeProfile(mode) {
|
||||
return mode?.origin === "user" || mode?.deletable === true;
|
||||
}
|
||||
|
||||
function isN7SyncedModeProfile(mode) {
|
||||
return mode?.origin === "n7_mqtt" || mode?.source_topic === "from_n7/scenes";
|
||||
}
|
||||
|
||||
function isDynamicModeProfile(mode) {
|
||||
const behavior = String(mode?.behavior || "").trim().toLowerCase();
|
||||
if (["cyclic", "dynamic"].includes(behavior)) return true;
|
||||
@@ -5179,10 +5326,14 @@ function createModeGalleryCard(mode) {
|
||||
heading.className = "mode-gallery-card-heading";
|
||||
const name = document.createElement("strong");
|
||||
name.textContent = mode.name || mode.id;
|
||||
const sourceBadge = document.createElement("span");
|
||||
sourceBadge.className = "mode-gallery-source-badge";
|
||||
sourceBadge.textContent = "N7 同步";
|
||||
sourceBadge.hidden = !isN7SyncedModeProfile(mode);
|
||||
const stateLabel = document.createElement("small");
|
||||
stateLabel.className = "mode-gallery-state";
|
||||
stateLabel.textContent = "点击执行";
|
||||
heading.append(name, stateLabel);
|
||||
heading.append(name, sourceBadge, stateLabel);
|
||||
const description = document.createElement("span");
|
||||
description.className = "mode-gallery-card-description";
|
||||
description.textContent = mode.description || "整车场景联动配置";
|
||||
@@ -5382,6 +5533,93 @@ function refreshModeProfileFromResult(result) {
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function modeProfileSyncCount(value) {
|
||||
const count = Number(value);
|
||||
return Number.isFinite(count) && count >= 0 ? Math.trunc(count) : 0;
|
||||
}
|
||||
|
||||
function primeModeProfileSyncFromRuntime(runtime) {
|
||||
const sync = state.modeProfileSync;
|
||||
const catalogRevision = String(
|
||||
runtime?.mode_scene_catalog?.catalog_revision || "",
|
||||
);
|
||||
if (catalogRevision) sync.catalogRevision = catalogRevision;
|
||||
if (sync.initialized) return;
|
||||
const ingress = runtime?.vehicle_mqtt?.mode_scene_import;
|
||||
if (!ingress || typeof ingress !== "object") return;
|
||||
sync.importedCount = modeProfileSyncCount(ingress.imported_count);
|
||||
sync.rejectedCount = modeProfileSyncCount(ingress.rejected_count);
|
||||
sync.initialized = true;
|
||||
}
|
||||
|
||||
async function checkModeProfileSync() {
|
||||
const sync = state.modeProfileSync;
|
||||
if (document.visibilityState === "hidden" || sync.inFlight) return;
|
||||
sync.inFlight = true;
|
||||
try {
|
||||
const response = await apiRequest("/api/mode-profiles/status", {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (response.status === 401) {
|
||||
window.location.replace("/login");
|
||||
return;
|
||||
}
|
||||
if (!response.ok) throw await apiResponseError(response);
|
||||
const payload = await response.json();
|
||||
const ingress = payload?.mode_scene_ingress || {};
|
||||
const catalogRevision = String(payload?.catalog_revision || "");
|
||||
const importedCount = modeProfileSyncCount(ingress.imported_count);
|
||||
const rejectedCount = modeProfileSyncCount(ingress.rejected_count);
|
||||
if (!sync.initialized) {
|
||||
sync.catalogRevision = catalogRevision;
|
||||
sync.importedCount = importedCount;
|
||||
sync.rejectedCount = rejectedCount;
|
||||
sync.initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const catalogChanged = Boolean(
|
||||
catalogRevision && catalogRevision !== sync.catalogRevision,
|
||||
);
|
||||
const imported = importedCount > sync.importedCount;
|
||||
const rejected = rejectedCount > sync.rejectedCount;
|
||||
sync.catalogRevision = catalogRevision || sync.catalogRevision;
|
||||
sync.importedCount = importedCount;
|
||||
sync.rejectedCount = rejectedCount;
|
||||
|
||||
const sceneId = String(ingress.last_scene_id || "").trim();
|
||||
const sceneAlreadyVisible = sceneId
|
||||
? modeProfiles().some((mode) => String(mode?.id || "") === sceneId)
|
||||
: false;
|
||||
if (catalogChanged || (imported && !sceneAlreadyVisible)) {
|
||||
await loadRuntime();
|
||||
}
|
||||
if (imported) {
|
||||
if (sceneId) revealModeProfile(sceneId);
|
||||
const sceneName = String(
|
||||
ingress.last_result?.scene_name || sceneId || "新模式",
|
||||
);
|
||||
showToast(`已从 N7 同步模式卡片:${sceneName}`);
|
||||
} else if (rejected) {
|
||||
const reason = String(ingress.last_error || "消息格式不符合要求");
|
||||
showToast(`N7 模式卡片导入失败:${reason}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("N7 mode profile synchronization check failed", error);
|
||||
} finally {
|
||||
sync.inFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
function startModeProfileSyncPolling() {
|
||||
const sync = state.modeProfileSync;
|
||||
window.clearInterval(sync.poller);
|
||||
sync.poller = window.setInterval(
|
||||
() => void checkModeProfileSync(),
|
||||
MODE_PROFILE_SYNC_POLL_INTERVAL_MS,
|
||||
);
|
||||
}
|
||||
|
||||
function openModeGallery() {
|
||||
const backdrop = $("#modeGalleryBackdrop");
|
||||
if (!backdrop) return;
|
||||
@@ -6639,8 +6877,16 @@ function setModeChipsDisabled(disabled) {
|
||||
});
|
||||
}
|
||||
|
||||
function hasExecutableModeCommandFlow(mode) {
|
||||
const flow = mode?.command_flow;
|
||||
if (!mode?.id || !flow || typeof flow !== "object") return false;
|
||||
const plans = flow.plans;
|
||||
return Boolean(flow.source_request_id)
|
||||
|| (plans && typeof plans === "object" && Object.keys(plans).length > 0);
|
||||
}
|
||||
|
||||
async function executeSavedModeProfile(mode, { command = "" } = {}) {
|
||||
if (!mode?.id || !mode?.command_flow?.source_request_id) return false;
|
||||
if (!hasExecutableModeCommandFlow(mode)) return false;
|
||||
const modeCommand = String(command || `进入${mode.name || mode.id}`).trim();
|
||||
const assistant = window.CognitiveAgentAssistant;
|
||||
const driverOnlyCh1 = isDriverOnlyCh1Runtime();
|
||||
@@ -6747,7 +6993,7 @@ function applyModeCommand(command, modeProfileId = null) {
|
||||
setModeRailSelection(mode);
|
||||
closeModeGallery({ restoreFocus: false });
|
||||
assistant?.open?.();
|
||||
if (mode?.command_flow?.source_request_id) {
|
||||
if (hasExecutableModeCommandFlow(mode)) {
|
||||
void executeSavedModeProfile(mode, { command: modeCommand });
|
||||
return true;
|
||||
}
|
||||
@@ -6809,6 +7055,7 @@ async function loadRuntime() {
|
||||
const response = await apiRequest("/api/runtime", { cache: "no-store" });
|
||||
if (!response.ok) throw await apiResponseError(response);
|
||||
state.runtime = await response.json();
|
||||
primeModeProfileSyncFromRuntime(state.runtime);
|
||||
renderControlTransport();
|
||||
applyRuntimeTargetPolicy(state.runtime);
|
||||
populateVehicleSelector();
|
||||
@@ -7157,6 +7404,8 @@ function resetSeatState() {
|
||||
|
||||
function resetVehicleStatusDisplay() {
|
||||
state.seatActuatorState = {};
|
||||
state.seatRenderFingerprint = "";
|
||||
state.domainRenderFingerprint = "";
|
||||
resetSeatState();
|
||||
const setText = (selector, value) => {
|
||||
const element = $(selector);
|
||||
@@ -7227,6 +7476,28 @@ function resetVehicleStatusDisplay() {
|
||||
summarySystem.textContent = "等待状态";
|
||||
summarySystem.dataset.state = "waiting";
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-state-rendered", {
|
||||
detail: { scope: "reset", fingerprint: "" },
|
||||
}));
|
||||
}
|
||||
|
||||
const VEHICLE_RENDER_VOLATILE_KEYS = new Set([
|
||||
"age_ms",
|
||||
"capture_sequence",
|
||||
"captured_at",
|
||||
"frame_timestamp",
|
||||
"last_seen_at",
|
||||
"observed_at",
|
||||
"received_at",
|
||||
"request_id",
|
||||
"timestamp",
|
||||
"updated_at",
|
||||
]);
|
||||
|
||||
function vehicleRenderFingerprint(value) {
|
||||
return JSON.stringify(value, (key, item) => (
|
||||
VEHICLE_RENDER_VOLATILE_KEYS.has(key) ? undefined : item
|
||||
));
|
||||
}
|
||||
|
||||
function mergeSeatActuatorState(previousState, patchState) {
|
||||
@@ -7246,20 +7517,6 @@ function mergeSeatActuatorState(previousState, patchState) {
|
||||
mergedSeat.position = mergedPosition;
|
||||
return;
|
||||
}
|
||||
if (feature === "dynamic_mode") {
|
||||
const item = $(`.seat-card[data-seat="${seat}"] [data-dynamic-mode]`);
|
||||
if (!item) return;
|
||||
const running = value.state === "running";
|
||||
item.classList.toggle("running", running);
|
||||
const runtimeMinutes = Number(value.max_runtime_ms || 600000) / 60000;
|
||||
const runtimeLabel = Number.isInteger(runtimeMinutes)
|
||||
? String(runtimeMinutes)
|
||||
: runtimeMinutes.toFixed(1);
|
||||
$("b", item).textContent = running
|
||||
? `${value.name} · 循环中 · 最长${runtimeLabel}分钟`
|
||||
: "未运行";
|
||||
return;
|
||||
}
|
||||
mergedSeat[feature] = value && typeof value === "object" && !Array.isArray(value)
|
||||
? { ...(previousSeat[feature] || {}), ...value }
|
||||
: value;
|
||||
@@ -7270,13 +7527,15 @@ function mergeSeatActuatorState(previousState, patchState) {
|
||||
}
|
||||
|
||||
function renderSeatState(actuatorState) {
|
||||
if (!actuatorState) return;
|
||||
if (!actuatorState) return false;
|
||||
const mergedActuatorState = mergeSeatActuatorState(
|
||||
state.seatActuatorState,
|
||||
actuatorState,
|
||||
);
|
||||
const renderFingerprint = vehicleRenderFingerprint(mergedActuatorState);
|
||||
state.seatActuatorState = mergedActuatorState;
|
||||
resetSeatState();
|
||||
if (renderFingerprint === state.seatRenderFingerprint) return false;
|
||||
state.seatRenderFingerprint = renderFingerprint;
|
||||
Object.entries(mergedActuatorState).forEach(([seat, functions]) => {
|
||||
const displayFunctions = seat === "driver"
|
||||
? twinDeviceForSeat("driver", { driver: functions })
|
||||
@@ -7302,6 +7561,20 @@ function renderSeatState(actuatorState) {
|
||||
: "CH1 状态已同步";
|
||||
return;
|
||||
}
|
||||
if (feature === "dynamic_mode") {
|
||||
const item = $(`.seat-card[data-seat="${seat}"] [data-dynamic-mode]`);
|
||||
if (!item) return;
|
||||
const running = value.state === "running";
|
||||
item.classList.toggle("running", running);
|
||||
const runtimeMinutes = Number(value.max_runtime_ms || 600000) / 60000;
|
||||
const runtimeLabel = Number.isInteger(runtimeMinutes)
|
||||
? String(runtimeMinutes)
|
||||
: runtimeMinutes.toFixed(1);
|
||||
$("b", item).textContent = running
|
||||
? `${value.name} · 循环中 · 最长${runtimeLabel}分钟`
|
||||
: "未运行";
|
||||
return;
|
||||
}
|
||||
if (feature === "position") {
|
||||
Object.entries(value).forEach(([axis, motion]) => {
|
||||
const item = $(`.seat-card[data-seat="${seat}"] [data-axis="${axis}"]`);
|
||||
@@ -7335,10 +7608,17 @@ function renderSeatState(actuatorState) {
|
||||
});
|
||||
});
|
||||
updateSeatTwin(mergedActuatorState);
|
||||
window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-state-rendered", {
|
||||
detail: { scope: "seat", fingerprint: renderFingerprint },
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderDomainState(domainState) {
|
||||
if (!domainState) return;
|
||||
if (!domainState) return false;
|
||||
const renderFingerprint = vehicleRenderFingerprint(domainState);
|
||||
if (renderFingerprint === state.domainRenderFingerprint) return false;
|
||||
state.domainRenderFingerprint = renderFingerprint;
|
||||
const outletModeLabel = (value) => {
|
||||
const normalized = String(value || "").toLowerCase().replaceAll("-", "_");
|
||||
const matrixLabels = {
|
||||
@@ -7723,6 +8003,11 @@ function renderDomainState(domainState) {
|
||||
});
|
||||
}
|
||||
}
|
||||
requestSeatTwinRender();
|
||||
window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-state-rendered", {
|
||||
detail: { scope: "domain", fingerprint: renderFingerprint },
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderedConfig(result) {
|
||||
@@ -9041,6 +9326,15 @@ window.addEventListener("cognitive-agent:request-complete", (event) => {
|
||||
setModeExecutionState(modeProfileId, succeeded ? "succeeded" : "failed");
|
||||
});
|
||||
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.visibilityState === "visible") void checkModeProfileSync();
|
||||
});
|
||||
|
||||
window.addEventListener("pagehide", () => {
|
||||
window.clearInterval(state.modeProfileSync.poller);
|
||||
state.modeProfileSync.poller = null;
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
initGlassThemeToggle();
|
||||
initJointTwinLayout();
|
||||
@@ -9147,6 +9441,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
$$('[data-download]').forEach((button) => button.addEventListener("click", () => downloadArtifact(button.dataset.download)));
|
||||
await loadAuthStatus();
|
||||
await loadRuntime();
|
||||
startModeProfileSyncPolling();
|
||||
void checkModeProfileSync();
|
||||
if (!isDriverOnlyCh1Runtime()) {
|
||||
await loadHardwareAccessStatus({ silent: true });
|
||||
state.hardwareAccessPoller = window.setInterval(
|
||||
|
||||
Reference in New Issue
Block a user