merge: sync feature/N7Web into feature/N7Web-Tech
# Conflicts: # apps/web-console/service-worker.js # src/cognitive_agent/application/mode_profile.py # src/cognitive_agent/application/mode_scene_catalog.py # src/cognitive_agent/application/seat_service.py # tests/e2e/test_seat_web_api.py # tests/unit/test_assistant_frontend_contract.py # tests/unit/test_vue_frontend_contract.py
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) {
|
||||
@@ -5219,6 +5362,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;
|
||||
@@ -5285,10 +5432,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 || "整车场景联动配置";
|
||||
@@ -5516,6 +5667,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;
|
||||
@@ -6811,8 +7049,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();
|
||||
@@ -6934,7 +7180,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;
|
||||
}
|
||||
@@ -6996,6 +7242,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();
|
||||
@@ -7344,6 +7591,8 @@ function resetSeatState() {
|
||||
|
||||
function resetVehicleStatusDisplay() {
|
||||
state.seatActuatorState = {};
|
||||
state.seatRenderFingerprint = "";
|
||||
state.domainRenderFingerprint = "";
|
||||
resetSeatState();
|
||||
const setText = (selector, value) => {
|
||||
const element = $(selector);
|
||||
@@ -7414,6 +7663,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) {
|
||||
@@ -7433,20 +7704,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;
|
||||
@@ -7457,13 +7714,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 })
|
||||
@@ -7489,6 +7748,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}"]`);
|
||||
@@ -7522,10 +7795,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 = {
|
||||
@@ -7910,6 +8190,11 @@ function renderDomainState(domainState) {
|
||||
});
|
||||
}
|
||||
}
|
||||
requestSeatTwinRender();
|
||||
window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-state-rendered", {
|
||||
detail: { scope: "domain", fingerprint: renderFingerprint },
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderedConfig(result) {
|
||||
@@ -9265,6 +9550,15 @@ window.addEventListener("cognitive-agent:request-complete", (event) => {
|
||||
setModeExecutionState(modeProfileId, scheduleRunning ? "started" : 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();
|
||||
@@ -9371,6 +9665,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(
|
||||
|
||||
@@ -466,6 +466,9 @@ body.console-v2 .v2-twin-device-popover {
|
||||
background: oklch(0.16 0.045 285 / 0.74) !important;
|
||||
border-color: var(--glass-line-strong) !important;
|
||||
box-shadow: 0 10px 28px oklch(0.04 0.035 286 / 0.32), inset 0 1px 0 var(--glass-highlight) !important;
|
||||
}
|
||||
|
||||
body.console-v2 .v2-twin-device-popover {
|
||||
-webkit-backdrop-filter: blur(14px) saturate(1.18);
|
||||
backdrop-filter: blur(14px) saturate(1.18);
|
||||
}
|
||||
@@ -526,8 +529,6 @@ body.console-v2 .v2-pet-nfc-field output {
|
||||
background: oklch(0.11 0.035 286 / 0.42) !important;
|
||||
border-color: var(--glass-line-strong) !important;
|
||||
box-shadow: inset 0 1px 0 oklch(1 0 0 / 0.08) !important;
|
||||
-webkit-backdrop-filter: blur(10px) saturate(1.14);
|
||||
backdrop-filter: blur(10px) saturate(1.14);
|
||||
}
|
||||
|
||||
body.console-v2 :is(input, textarea, select):focus {
|
||||
|
||||
@@ -1265,6 +1265,7 @@
|
||||
pollTimer: null,
|
||||
deviceFrame: null,
|
||||
deviceFingerprint: "",
|
||||
deviceRenderDirty: true,
|
||||
activeDevice: null,
|
||||
pinnedDevice: null,
|
||||
previewClearTimer: null,
|
||||
@@ -1523,9 +1524,13 @@
|
||||
</label>`;
|
||||
}
|
||||
|
||||
function renderSyncedDevices() {
|
||||
function renderSyncedDevices({ force = false } = {}) {
|
||||
const grid = $("#v2DeviceSyncGrid");
|
||||
if (!grid) return;
|
||||
if (!grid) return false;
|
||||
if (!force && !panelVisible("devices")) {
|
||||
displaySync.deviceRenderDirty = true;
|
||||
return false;
|
||||
}
|
||||
grid.innerHTML = syncedDevices.map((device) => {
|
||||
const rows = deviceReadoutRows(device);
|
||||
const waiting = rows.some(([, value]) => value === "-");
|
||||
@@ -1553,7 +1558,9 @@
|
||||
$(`.v2-synced-device[data-device="${displaySync.activeDevice}"]`)?.classList.add("active");
|
||||
}
|
||||
displaySync.deviceFingerprint = JSON.stringify(syncedDevices.map((device) => [device.id, deviceReadoutRows(device)]));
|
||||
displaySync.deviceRenderDirty = false;
|
||||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||||
return true;
|
||||
}
|
||||
|
||||
function storeDeviceControlDraft(input) {
|
||||
@@ -1641,11 +1648,17 @@
|
||||
}
|
||||
|
||||
function scheduleSyncedDeviceRender() {
|
||||
if (!panelVisible("devices")) {
|
||||
displaySync.deviceRenderDirty = true;
|
||||
return;
|
||||
}
|
||||
if (displaySync.deviceFrame) return;
|
||||
displaySync.deviceFrame = requestAnimationFrame(() => {
|
||||
displaySync.deviceFrame = null;
|
||||
const fingerprint = JSON.stringify(syncedDevices.map((device) => [device.id, deviceReadoutRows(device)]));
|
||||
if (fingerprint === displaySync.deviceFingerprint && $("#v2DeviceSyncGrid")?.childElementCount) return;
|
||||
if (!displaySync.deviceRenderDirty
|
||||
&& fingerprint === displaySync.deviceFingerprint
|
||||
&& $("#v2DeviceSyncGrid")?.childElementCount) return;
|
||||
renderSyncedDevices();
|
||||
if (displaySync.activeDevice) presentTwinDevice(displaySync.activeDevice, {
|
||||
pinned: Boolean(displaySync.pinnedDevice),
|
||||
@@ -1748,7 +1761,11 @@
|
||||
rightRail.toggleAttribute("inert", collapsed);
|
||||
rightRail.dataset.openPanels = openPanels.join(" ") || "none";
|
||||
}
|
||||
if (key === "devices" && visible) renderSyncedDevices({ force: true });
|
||||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||||
window.dispatchEvent(new CustomEvent("cognitive-agent:side-panel-visibility", {
|
||||
detail: { panel: key, visible: Boolean(visible) },
|
||||
}));
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
}
|
||||
|
||||
@@ -2550,19 +2567,10 @@
|
||||
}
|
||||
buildTwinDeviceInteractions();
|
||||
cabinBand.classList.add("v2-source-state-only");
|
||||
new MutationObserver(scheduleSyncedDeviceRender).observe(cabinBand, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
subtree: true,
|
||||
});
|
||||
const stateBand = $(".state-band");
|
||||
if (stateBand) new MutationObserver(scheduleSyncedDeviceRender).observe(stateBand, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
subtree: true,
|
||||
});
|
||||
window.addEventListener(
|
||||
"cognitive-agent:vehicle-state-rendered",
|
||||
scheduleSyncedDeviceRender,
|
||||
);
|
||||
}
|
||||
setSidePanelVisibility("devices", false);
|
||||
setSidePanelVisibility("extensions", false);
|
||||
@@ -2983,8 +2991,8 @@
|
||||
});
|
||||
window.addEventListener("cognitive-agent:vehicle-feedback-state", () => {
|
||||
syncRuntimePresentation();
|
||||
renderSyncedDevices();
|
||||
if (displaySync.activeDevice) {
|
||||
scheduleSyncedDeviceRender();
|
||||
if (panelVisible("devices") && displaySync.activeDevice) {
|
||||
presentTwinDevice(displaySync.activeDevice, {
|
||||
pinned: Boolean(displaySync.pinnedDevice),
|
||||
selectSeat: false,
|
||||
|
||||
@@ -869,7 +869,10 @@
|
||||
createLayer();
|
||||
seedHistory();
|
||||
renderMonitor();
|
||||
monitor.timer = window.setInterval(() => sampleNow(), SAMPLE_INTERVAL_MS);
|
||||
monitor.timer = window.setInterval(
|
||||
() => sampleNow({ render: monitor.open }),
|
||||
SAMPLE_INTERVAL_MS,
|
||||
);
|
||||
document.addEventListener("keydown", captureModalKeys, true);
|
||||
window.addEventListener("cognitive-agent:power-telemetry", (event) => ingestPowerTelemetry(event.detail || {}, { sample: false }));
|
||||
const existingUpdater = window.updateDevicePowerTelemetry;
|
||||
|
||||
@@ -38,14 +38,14 @@
|
||||
<script src="/vendor/three/0.147.0/fflate.min.js" defer></script>
|
||||
<script src="/vendor/three/0.147.0/FBXLoader.js" defer></script>
|
||||
<script src="/network.js?v=android-pad-2" defer></script>
|
||||
<script src="/app.js?v=dual-seat-141" defer></script>
|
||||
<script src="/app.js?v=dual-seat-145-n7-dispatch" defer></script>
|
||||
<script src="/speech-input.js?v=sensevoice-13" defer></script>
|
||||
<script type="module" src="/gesture-control.js?v=android-pad-9"></script>
|
||||
<script src="/pwa.js?v=android-pad-3" defer></script>
|
||||
<script src="/console-v2.js?v=local-first-76" defer></script>
|
||||
<script src="/self-extension.js?v=pet-settings-52" defer></script>
|
||||
<script src="/console-v2.js?v=local-first-77-performance" defer></script>
|
||||
<script src="/self-extension.js?v=pet-settings-53-performance" defer></script>
|
||||
<script src="/assistant-chat.js?v=codex-assistant-34" defer></script>
|
||||
<script src="/energy-monitor.js?v=energy-monitor-4" defer></script>
|
||||
<script src="/energy-monitor.js?v=energy-monitor-5-performance" defer></script>
|
||||
</head>
|
||||
<body class="console-v2">
|
||||
<div id="app" aria-busy="true"></div>
|
||||
|
||||
@@ -389,6 +389,11 @@
|
||||
let selectedPetCardId = null;
|
||||
const petCardDrafts = new Map();
|
||||
let nfcPollTimer = null;
|
||||
let nfcDialogFingerprint = "";
|
||||
let nfcDialogDirty = true;
|
||||
let unifiedExtensionFingerprint = "";
|
||||
let unifiedExtensionDirty = true;
|
||||
let gscuHealthFingerprint = "";
|
||||
let legacySameOriginChecked = false;
|
||||
let legacyMigrationCompleted = false;
|
||||
const legacyBridgeAttempts = new Set();
|
||||
@@ -3673,12 +3678,50 @@
|
||||
return reader?.error || "请连接 NFC 读卡器并确认 CH340 / CH341 驱动可用";
|
||||
}
|
||||
|
||||
function renderNfcDialog() {
|
||||
function currentNfcDialogFingerprint() {
|
||||
return JSON.stringify({
|
||||
revision: nfcSnapshot.revision,
|
||||
reader: nfcSnapshot.reader,
|
||||
card: nfcSnapshot.card,
|
||||
petRevision: petCardSnapshot.revision,
|
||||
pets: petCardSnapshot.cards,
|
||||
publisher: petCardSnapshot.publisher,
|
||||
devices: state.devices.map((device) => ({
|
||||
id: device.id,
|
||||
name: device.name,
|
||||
type: device.type,
|
||||
cardUid: device.cardUid || null,
|
||||
})),
|
||||
capabilities: state.capabilities.map((capability) => ({
|
||||
id: capability.id,
|
||||
deviceId: capability.deviceId,
|
||||
name: capability.name,
|
||||
aliases: capability.aliases,
|
||||
slot: capability.slot,
|
||||
status: capability.status,
|
||||
verified: capability.verified,
|
||||
enabled: capability.enabled !== false,
|
||||
})),
|
||||
infraredCommands: state.infraredCommands,
|
||||
});
|
||||
}
|
||||
|
||||
function renderNfcDialog({ force = false } = {}) {
|
||||
const dialog = $("#extensionNfcDialog");
|
||||
if (!dialog) return false;
|
||||
if (!force && !dialog.open) {
|
||||
nfcDialogDirty = true;
|
||||
return false;
|
||||
}
|
||||
const renderFingerprint = currentNfcDialogFingerprint();
|
||||
if (!force && !nfcDialogDirty && renderFingerprint === nfcDialogFingerprint) {
|
||||
return false;
|
||||
}
|
||||
const stage = $("#extensionNfcStage");
|
||||
const body = $("#extensionNfcBody");
|
||||
const status = $("#extensionNfcReaderStatus");
|
||||
const badge = $("#extensionNfcReaderBadge");
|
||||
if (!stage || !body || !status || !badge) return;
|
||||
if (!stage || !body || !status || !badge) return false;
|
||||
|
||||
const reader = nfcSnapshot.reader || {};
|
||||
const card = nfcSnapshot.card;
|
||||
@@ -3725,7 +3768,10 @@
|
||||
${capabilities.length ? `<label class="v2-nfc-select-all"><input type="checkbox" data-nfc-select-all ${allEnabled ? "checked" : ""} /><span><strong>全选</strong><small>统一允许或禁止当前设备 ID 下的全部命令</small></span></label><div class="v2-nfc-command-list">${capabilities.map((capability) => `<label class="v2-nfc-command-option"><input type="checkbox" data-nfc-capability-enabled="${escapeHtml(capability.id)}" ${capability.enabled !== false ? "checked" : ""} /><span><strong>${escapeHtml(capability.name)}</strong><small>IR ${String(capability.slot).padStart(3, "0")} · ${escapeHtml((capability.aliases || []).join("、"))}</small></span></label>`).join("")}</div>` : `<div class="v2-nfc-no-devices"><i data-lucide="triangle-alert"></i><div><strong>该设备 ID 暂无有效命令</strong><span>槽位可能已删除或改绑到其他设备 ID,请重新检查红外设置。</span></div></div>`}
|
||||
<div class="v2-nfc-recognized-actions"><span>复选框状态会持久化为自然语言执行白名单。</span><button type="button" data-nfc-unbind>解绑 NFC</button></div>`;
|
||||
}
|
||||
nfcDialogFingerprint = renderFingerprint;
|
||||
nfcDialogDirty = false;
|
||||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildUnifiedNfcDialog() {
|
||||
@@ -3891,8 +3937,8 @@
|
||||
}
|
||||
|
||||
function openNfcDialog() {
|
||||
renderNfcDialog();
|
||||
openDialog("extensionNfcDialog");
|
||||
renderNfcDialog({ force: true });
|
||||
void refreshNfcStatus();
|
||||
}
|
||||
|
||||
@@ -3969,10 +4015,73 @@
|
||||
if (!mount) return false;
|
||||
let snapshot = null;
|
||||
|
||||
const render = () => {
|
||||
const extensionViewFingerprint = (health) => JSON.stringify({
|
||||
transport: health.transport,
|
||||
connected: health.connected,
|
||||
peerConnected: health.peer_connected,
|
||||
peer: health.peer,
|
||||
listening: health.listening,
|
||||
message: health.message,
|
||||
extensionCatalog: (snapshot?.extension?.catalog || []).map((item) => ({
|
||||
id: item.id,
|
||||
deviceId: item.device_id,
|
||||
deviceName: item.device_name,
|
||||
deviceType: item.device_type,
|
||||
typeLabel: item.type_label,
|
||||
action: item.action,
|
||||
name: item.name,
|
||||
cardUid: item.card_uid,
|
||||
status: item.status,
|
||||
enabled: item.enabled,
|
||||
})),
|
||||
lastAction: snapshot?.extension?.last_action ? {
|
||||
command: snapshot.extension.last_action.command,
|
||||
type: snapshot.extension.last_action.type,
|
||||
} : null,
|
||||
lastResult: snapshot?.extension?.result ? {
|
||||
status: snapshot.extension.result.status,
|
||||
message: snapshot.extension.result.message,
|
||||
} : null,
|
||||
nfc: {
|
||||
revision: nfcSnapshot.revision,
|
||||
reader: nfcSnapshot.reader,
|
||||
card: nfcSnapshot.card,
|
||||
},
|
||||
pets: petCardSnapshot,
|
||||
localDevices: state.devices.map((device) => ({
|
||||
id: device.id,
|
||||
name: device.name,
|
||||
type: device.type,
|
||||
cardUid: device.cardUid || null,
|
||||
})),
|
||||
localCapabilities: state.capabilities.map((capability) => ({
|
||||
id: capability.id,
|
||||
deviceId: capability.deviceId,
|
||||
name: capability.name,
|
||||
slot: capability.slot,
|
||||
status: capability.status,
|
||||
verified: capability.verified,
|
||||
enabled: capability.enabled !== false,
|
||||
})),
|
||||
infraredCommands: state.infraredCommands,
|
||||
});
|
||||
|
||||
const render = ({ force = false } = {}) => {
|
||||
const health = snapshot?.extension_health || {};
|
||||
healthState = health;
|
||||
if ($("#extensionGscuTcpDialog")?.open) renderGscuTcpDialog(health, false);
|
||||
const nextGscuHealthFingerprint = JSON.stringify({
|
||||
transport: health.transport,
|
||||
connected: health.connected,
|
||||
peerConnected: health.peer_connected,
|
||||
peer: health.peer,
|
||||
listening: health.listening,
|
||||
message: health.message,
|
||||
});
|
||||
if ($("#extensionGscuTcpDialog")?.open
|
||||
&& (force || nextGscuHealthFingerprint !== gscuHealthFingerprint)) {
|
||||
renderGscuTcpDialog(health, false);
|
||||
}
|
||||
gscuHealthFingerprint = nextGscuHealthFingerprint;
|
||||
const embeddedNfc = health.nfc;
|
||||
if (nfcSnapshot.revision < 0 && embeddedNfc) {
|
||||
nfcSnapshot = {
|
||||
@@ -3989,6 +4098,16 @@
|
||||
}
|
||||
}
|
||||
if (health.pet_cards) applyPetCardSnapshot(health.pet_cards, { render: false });
|
||||
const panel = mount.closest("#v2ExtensionPanel");
|
||||
if (!force && (panel?.hidden || panel?.classList.contains("is-collapsed"))) {
|
||||
unifiedExtensionDirty = true;
|
||||
return false;
|
||||
}
|
||||
const renderFingerprint = extensionViewFingerprint(health);
|
||||
if (!force
|
||||
&& !unifiedExtensionDirty
|
||||
&& renderFingerprint === unifiedExtensionFingerprint
|
||||
&& mount.childElementCount) return false;
|
||||
const physicalConnected = health.transport !== "simulation"
|
||||
&& Boolean(health.connected || health.peer_connected);
|
||||
const listening = Boolean(health.listening);
|
||||
@@ -4114,7 +4233,10 @@
|
||||
<i data-lucide="workflow"></i><div><small>最近联动</small><strong>${escapeHtml(visibleLastAction?.command || visibleLastAction?.type || "等待 NFC 外设感应")}</strong></div>
|
||||
<span>${escapeHtml(visibleLastAction ? (lastResult?.status || lastResult?.message || "STANDBY") : "STANDBY")}</span>
|
||||
</footer>`;
|
||||
unifiedExtensionFingerprint = renderFingerprint;
|
||||
unifiedExtensionDirty = false;
|
||||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||||
return true;
|
||||
};
|
||||
renderUnifiedExtensionView = render;
|
||||
|
||||
@@ -4125,6 +4247,11 @@
|
||||
}
|
||||
render();
|
||||
});
|
||||
window.addEventListener("cognitive-agent:side-panel-visibility", (event) => {
|
||||
if (event.detail?.panel === "extensions" && event.detail?.visible) {
|
||||
render({ force: true });
|
||||
}
|
||||
});
|
||||
buildDialogs();
|
||||
buildUnifiedNfcDialog();
|
||||
bindUnifiedNfcEvents(mount);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const CACHE_VERSION = "cognitive-agent-pad-v318-tech-nfc-routing";
|
||||
const CACHE_VERSION = "cognitive-agent-pad-v322-tech-n7-dispatch";
|
||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
||||
const APP_SHELL = [
|
||||
@@ -6,22 +6,22 @@ const APP_SHELL = [
|
||||
"/index.html",
|
||||
"/styles.css?v=dual-seat-23",
|
||||
"/network.js?v=android-pad-2",
|
||||
"/app.js?v=dual-seat-141",
|
||||
"/app.js?v=dual-seat-145-n7-dispatch",
|
||||
"/gesture-control.js?v=android-pad-9",
|
||||
"/pwa.js?v=android-pad-3",
|
||||
"/console-v2.css?v=delivery-state-69",
|
||||
"/console-v2.js?v=local-first-76",
|
||||
"/console-v2.js?v=local-first-77-performance",
|
||||
"/self-extension.css?v=pet-settings-52",
|
||||
"/self-extension.js?v=pet-settings-52",
|
||||
"/self-extension.js?v=pet-settings-53-performance",
|
||||
"/speech-input.css?v=sensevoice-7",
|
||||
"/speech-input.js?v=sensevoice-13",
|
||||
"/speech-audio-worklet.js?v=sensevoice-5",
|
||||
"/assistant-chat.css?v=codex-assistant-29",
|
||||
"/assistant-chat.js?v=codex-assistant-34",
|
||||
"/energy-monitor.css?v=energy-monitor-4",
|
||||
"/energy-monitor.js?v=energy-monitor-4",
|
||||
"/energy-monitor.js?v=energy-monitor-5-performance",
|
||||
"/ui-consistency.css?v=spatial-ui-8",
|
||||
"/codepen-glass-theme.css?v=creative-cloud-5",
|
||||
"/codepen-glass-theme.css?v=creative-cloud-6-performance",
|
||||
"/vue-ui.js?v=vue-presentation-3",
|
||||
"/assets/pets/self-extension-cat.png?v=3",
|
||||
"/assets/pets/self-extension-cat-entrance.png?v=3",
|
||||
|
||||
@@ -724,6 +724,22 @@ body.mode-editor-open {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mode-gallery-source-badge {
|
||||
flex: 0 0 auto;
|
||||
padding: 2px 5px;
|
||||
color: #315eb8;
|
||||
background: rgba(91, 132, 222, 0.14);
|
||||
border: 1px solid rgba(91, 132, 222, 0.24);
|
||||
border-radius: 999px;
|
||||
font-size: 7px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.mode-gallery-source-badge[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mode-gallery-card-description,
|
||||
.mode-gallery-card-meta {
|
||||
overflow: hidden;
|
||||
|
||||
Reference in New Issue
Block a user