diff --git a/.gitignore b/.gitignore index 98e0690..f9b9909 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ tmp/*.log firmware/t2g/Seat_Controller_demo/Tests/manual_hil/logs/ firmware/t2g/Seat_Controller_demo/Tests/manual_hil/seat_function_runtime/ artifacts/hil/*_server_*.log +artifacts/hil/frontend_service_*.log +tmp/can-matrix-artifact/ +tmp/reference-project-code-CAN_DEV/ firmware/t2g/Seat_Controller_demo/IAR/settings/ firmware/t2g/Seat_Controller_demo/IAR/Seat_Controller_demo.eww firmware/t2g/Seat_Controller_demo/IAR/rev_d/.Seat_Controller_demo_build_cache diff --git a/apps/web-console/README.md b/apps/web-console/README.md index 46eb028..537a5f0 100755 --- a/apps/web-console/README.md +++ b/apps/web-console/README.md @@ -47,6 +47,13 @@ make dev-lan `GSCU_TRANSPORT=mqtt` 与 `GSCU_MQTT_*` 配置接入 MQTT。MQTT 依赖可通过 项目的 `ir` 可选依赖安装。自扩展不再需要独立的 `8010` 前端服务。 +TCP 模式下本服务是服务器,GSCU1B 是客户端:设备配网页中的“TCP服务器” +必须填写运行本服务的主机地址,“TCP端口”必须与 `GSCU_TCP_PORT` 一致。 +如需绑定指定网卡地址,可设置 `GSCU_TCP_LISTEN_HOST`;网卡晚于服务就绪或 +端口被短暂占用时,监听器会按 `GSCU_TCP_BIND_RETRY_SECONDS`(默认 1 秒) +自动重试。`/api/extension/health` 的 `listening` 表示服务器已监听,只有收到 +真实设备会话后 `connected`/`peer_connected` 才会变为 `true`。 + ## N7 自标定 自标定前端、原始 Android `n7_seat.glb`、四轴 Hall 标定协议以及 diff --git a/apps/web-console/app.js b/apps/web-console/app.js index 0c3bf67..d0e2191 100755 --- a/apps/web-console/app.js +++ b/apps/web-console/app.js @@ -1,5 +1,20 @@ +function createOpaqueId(prefix) { + if (globalThis.crypto?.randomUUID) { + return `${prefix}-${globalThis.crypto.randomUUID()}`; + } + if (globalThis.crypto?.getRandomValues) { + const bytes = new Uint8Array(16); + globalThis.crypto.getRandomValues(bytes); + const randomHex = [...bytes] + .map((value) => value.toString(16).padStart(2, "0")) + .join(""); + return `${prefix}-${randomHex}`; + } + return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 14)}`; +} + function createConversationId() { - return `conv-${crypto.randomUUID()}`; + return createOpaqueId("conv"); } function loadConversationId() { @@ -14,7 +29,7 @@ function loadHardwareClientId() { const key = "cognitive-agent.hardware-client-id"; const stored = localStorage.getItem(key); if (/^hwc-[a-z0-9-]{8,80}$/.test(stored || "")) return stored; - const clientId = `hwc-${crypto.randomUUID()}`; + const clientId = createOpaqueId("hwc"); localStorage.setItem(key, clientId); return clientId; } @@ -26,6 +41,8 @@ const state = { vehicleId: null, runtime: null, result: null, + vehicleStateSnapshot: null, + vehicleStateTurn: -1, progressTimer: null, seatTwin: null, seatActuatorState: {}, @@ -1730,10 +1747,11 @@ function renderTwinCalloutValues(device) { function renderTwinReadout() { const twin = state.seatTwin; if (!twin) return; - const seat = "passenger"; - const device = twin.actuatorState.passenger; - $("#twinSeatName").textContent = "副驾"; - $("#twinSeatZone").textContent = "front_right"; + const driverOnly = state.runtime?.vehicle_can?.target_policy === "ch1_body_can_driver_only"; + const seat = driverOnly ? "driver" : "passenger"; + const device = twin.actuatorState[seat] || twin.actuatorState.passenger; + $("#twinSeatName").textContent = driverOnly ? "主驾" : "副驾"; + $("#twinSeatZone").textContent = driverOnly ? "front_left · CH1" : "front_right"; const postureAngles = twinPostureAngles(device); Object.entries(TWIN_DEFAULT_POSITIONS).forEach(([axis, fallback]) => { const motion = device?.position?.[axis]; @@ -3388,6 +3406,12 @@ function isLocalDecisionHost() { return ["127.0.0.1", "localhost", "::1"].includes(window.location.hostname); } +function isDriverOnlyCh1Runtime() { + return state.runtime?.vehicle_can?.configured === true + && state.runtime?.vehicle_can?.target_policy === "ch1_body_can_driver_only" + && state.runtime?.hardware?.configured !== true; +} + function renderHardwareAccess(status) { state.hardwareAccess = status; } @@ -3415,6 +3439,10 @@ function selectProvider(provider) { function updateSeatFeedbackModeLabel() { const label = $("#seatFeedbackModeLabel"); if (!label) return; + if (state.seatFeedbackSource === "physical_can_ch1") { + label.textContent = "CH1 PHYSICAL CAN · DRIVER SYNCHRONIZED"; + return; + } if (state.seatFeedbackSource === "physical_can") { label.textContent = "PHYSICAL CAN · BENCH SYNCHRONIZED"; return; @@ -3445,9 +3473,8 @@ function selectExecutionMode(mode) { }); const hardware = mode === "hardware"; const simulatedHardware = mode === "simulated_hardware"; - const approvalRequired = Boolean(state.runtime?.hardware?.approval_required); const vehicleCanReady = Boolean(state.runtime?.vehicle_can?.configured); - $("#approvalField").hidden = !hardware || !approvalRequired; + const ch1DriverOnly = state.runtime?.vehicle_can?.target_policy === "ch1_body_can_driver_only"; $("#simulationFaultField").hidden = !simulatedHardware; $("#executionBadge").className = `status-badge ${hardware ? "" : simulatedHardware ? "warning" : "neutral"}`.trim(); $("#executionBadge").innerHTML = hardware @@ -3456,10 +3483,12 @@ function selectExecutionMode(mode) { ? 'SIM · 模拟台架' : 'DRY-RUN'; $("#safetyTitle").textContent = hardware - ? "真实台架安全门禁" + ? (ch1DriverOnly ? "CH1 实车安全门禁" : "真实台架安全门禁") : simulatedHardware ? "模拟台架隔离边界" : "虚拟执行边界"; $("#safetyText").textContent = hardware - ? vehicleCanReady + ? ch1DriverOnly && vehicleCanReady + ? "仅使用 ZLG CH1 控制主驾座椅与已验证车身设备;每次动作前必须确认 P 挡和 0 km/h,并以 CH1 状态帧回读为准。CH0、副驾 T2G 与 IAR 路径均不参与。" + : vehicleCanReady ? "同一 CAN 盒由单进程持有:通道 0 控制副驾座椅并校验板端反馈,通道 1 以 ISO CAN FD 500 kbit/s / 2 Mbit/s 控制空调等车身设备并校验状态帧。" : "当前仅通道 0 连接副驾座椅台架并校验板端反馈;车端 CAN 未启用,模式中的其他车舱领域保持虚拟执行。" : simulatedHardware @@ -3467,14 +3496,16 @@ function selectExecutionMode(mode) { : "四域 CAN 均为 dry-run,真实发送被强制关闭。"; $("#executeStageLabel").textContent = hardware ? "台架执行" : simulatedHardware ? "故障仿真" : "虚拟执行"; $("#feedbackModeLabel").textContent = hardware - ? "T2G BENCH · PHYSICAL CAN FEEDBACK" + ? (ch1DriverOnly + ? "CH1 BODY CAN · AUTHORITATIVE PHYSICAL FEEDBACK" + : "T2G BENCH · PHYSICAL CAN FEEDBACK") : simulatedHardware ? "SIMULATED HIL · FAULT INJECTION" : "MATRIX-BACKED · SIMULATED FEEDBACK"; const runButtonLabel = $("#runButton span"); if (runButtonLabel) { runButtonLabel.textContent = hardware - ? (approvalRequired ? "授权并真实执行" : "真实执行") + ? "真实执行" : simulatedHardware ? "运行故障仿真" : "生成并执行"; } updateSeatFeedbackModeLabel(); @@ -3571,7 +3602,11 @@ function selectVehicle(vehicleId, { silent = false } = {}) { $("#vehicleNote").textContent = longitudinal && backrest ? `${vehicle.name} · 水平 ${longitudinal.range}${longitudinal.unit} · 靠背 ${backrest.range}${backrest.unit}` : `${vehicle.name} · 已加载车型配置`; - if (state.result?.actuator_state) { + if (isDriverOnlyCh1Runtime()) { + if (state.vehicleStateSnapshot?.actuatorState) { + renderSeatState(state.vehicleStateSnapshot.actuatorState); + } + } else if (state.result?.actuator_state) { renderSeatState(state.result.actuator_state); } else if (state.seatTwin?.actuatorState) { renderSeatState(state.seatTwin.actuatorState); @@ -3613,6 +3648,14 @@ function modeSecondsLabel(durationMs) { function populateModeChips() { const group = $(".mode-chips"); if (!group) return; + if (state.runtime?.vehicle_can?.target_policy === "ch1_body_can_driver_only") { + group.innerHTML = ` + + + `; + refreshIcons(); + return; + } const buttons = () => $$("button", group); modeProfiles().forEach((mode) => { const label = modeChipLabel(mode); @@ -3709,17 +3752,91 @@ function saveCurrentModeProfile() { $("#requestForm").requestSubmit(); } +function applyRuntimeTargetPolicy(runtime) { + const driverOnly = runtime?.vehicle_can?.target_policy === "ch1_body_can_driver_only"; + if (!driverOnly) return; + const stage = $("#seatTwin"); + const canvas = $("#seatTwinCanvas"); + const label = $(".twin-seat-label"); + const card = $(".seat-card[data-seat=\"passenger\"], .seat-card[data-seat=\"driver\"]"); + if (stage) stage.dataset.physicalSeat = "driver"; + if (canvas) { + canvas.dataset.activeSeat = "front-left"; + canvas.setAttribute("aria-label", "通过模型动画展示 CH1 主驾座椅与车身设备物理反馈"); + } + if (label) { + label.classList.remove("passenger"); + label.classList.add("driver"); + $("span", label).textContent = "主驾车身座椅"; + $("small", label).textContent = "ACTIVE · FRONT LEFT · CH1"; + } + $("#twinSeatName").textContent = "主驾"; + $("#twinSeatZone").textContent = "front_left"; + if (card) { + card.dataset.seat = "driver"; + $(".seat-identity strong", card).textContent = "主驾"; + $(".seat-identity span", card).textContent = "front_left · CH1"; + } + const sectionHeading = $(".seat-state-section .section-title-row h3"); + if (sectionHeading) sectionHeading.textContent = "主驾座椅状态"; + const coverageTarget = $(".coverage-section dl div:nth-child(4) dt"); + const coverageValue = $(".coverage-section dl div:nth-child(4) dd"); + if (coverageTarget) coverageTarget.textContent = "主驾座椅车身 CAN 执行器"; + if (coverageValue) { + coverageValue.textContent = "driver · front_left · CH1"; + coverageValue.className = "available"; + } + const jointCopy = $(".joint-twin-heading p"); + if (jointCopy) jointCopy.textContent = "CH1 主驾座椅与车身设备物理反馈联合展示;CH0、副驾 T2G 与 IAR 路径已禁用。"; + const unsupportedFrontLightPanel = $('[data-domain-panel="front_light"]'); + if (unsupportedFrontLightPanel) unsupportedFrontLightPanel.hidden = true; + const jointSeatScope = $(".joint-scope span"); + if (jointSeatScope) jointSeatScope.innerHTML = '主驾座椅 · CH1'; + const input = $("#utterance"); + if (input) { + input.placeholder = "例如:打开主驾通风,并把空调温度调到24度"; + if (input.value.includes("观影状态") || input.value.includes("副驾")) { + input.value = "打开主驾通风,并把空调温度调到24度"; + } + } + const safeExamples = [ + ["seat driver vent low", "主驾通风低档"], + ["seat driver heat low", "主驾加热低档"], + ["ac temp driver 24", "主驾空调 24℃"], + ["window front-left position 0", "关闭左前车窗"], + ["shade position 0", "关闭遮阳帘"], + ["light reading all off", "关闭全部阅读灯"], + ]; + $$(".example-command").forEach((button, index) => { + const example = safeExamples[index]; + button.hidden = !example; + if (!example) return; + button.dataset.example = example[0]; + const label = $("span", button); + if (label) label.textContent = example[1]; + }); + const gestureButton = $("#gestureToggleButton"); + if (gestureButton) { + gestureButton.disabled = true; + gestureButton.title = "CH1 主驾实车测试不启用无闭环的座椅位置手势"; + } + refreshIcons(); +} + async function loadRuntime() { try { const response = await apiRequest("/api/runtime", { cache: "no-store" }); if (!response.ok) throw new Error(`HTTP ${response.status}`); state.runtime = await response.json(); + applyRuntimeTargetPolicy(state.runtime); populateVehicleSelector(); populateModeChips(); const hardware = state.runtime.hardware || {}; const vehicleCan = state.runtime.vehicle_can || {}; const linkedHardwareReady = Boolean(hardware.configured && vehicleCan.configured); const seatHardwareReady = Boolean(hardware.configured); + const vehicleHardwareReady = Boolean(vehicleCan.configured); + const ch1DriverOnly = vehicleCan.target_policy === "ch1_body_can_driver_only"; const hardwareExecutionReady = Boolean(state.runtime.hardware_execution_allowed); const badge = $("#modelBadge"); const gptButton = $('[data-provider="openai"]'); @@ -3747,34 +3864,39 @@ async function loadRuntime() { : "通过安卓联调脚本或环境变量启用模拟台架"; const profileText = (hardware.firmware_profiles || []).join(" / ") || "未选择 profile"; const flashText = hardware.auto_flash_enabled ? "自动烧录已启用" : "自动烧录未启用"; - const approvalText = hardware.approval_required ? "需授权令牌" : "调试免令牌"; const concurrencyText = hardware.concurrency_policy === "single_flight_fail_fast" ? " · 动作防冲突" : ""; - const accessText = hardware.access_policy === "local_approval_idle_lease" + const accessText = !ch1DriverOnly + && hardware.access_policy === "local_approval_idle_lease" ? ` · 本地审批 · 空闲 ${hardware.access_inactivity_timeout_seconds || 180} 秒自动停止` : ""; - $("#hardwareNote").textContent = linkedHardwareReady - ? `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s · CH${vehicleCan.channel} 车身 CAN FD ${vehicleCan.arbitration.bitrate / 1000}k/${vehicleCan.data.bitrate / 1000000}M · ${profileText} · ${flashText} · ${approvalText}${accessText}${concurrencyText}` + $("#hardwareNote").textContent = ch1DriverOnly && vehicleHardwareReady && !seatHardwareReady + ? `${vehicleCan.backend} · CH${vehicleCan.channel} 车身 CAN FD ${vehicleCan.arbitration.bitrate / 1000}k/${vehicleCan.data.bitrate / 1000000}M · 仅主驾/车端白名单 · CH0/T2G/IAR 已禁用${accessText}${concurrencyText}` + : linkedHardwareReady + ? `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s · CH${vehicleCan.channel} 车身 CAN FD ${vehicleCan.arbitration.bitrate / 1000}k/${vehicleCan.data.bitrate / 1000000}M · ${profileText} · ${flashText}${accessText}${concurrencyText}` : seatHardwareReady ? `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s · ${profileText} · ${flashText} · 车端 CAN 未启用,其他领域保持虚拟执行${accessText}${concurrencyText}` - : `联动真实执行已锁定:${profileText};请完成座椅固件、双通道 ZLG 适配器、车身 CAN FD${hardware.approval_required ? "和令牌" : ""}配置`; - $("#hardwareAliasNote").textContent = linkedHardwareReady + : `联动真实执行已锁定:${profileText};请完成座椅固件、双通道 ZLG 适配器和车身 CAN FD 配置`; + $("#hardwareAliasNote").textContent = ch1DriverOnly && vehicleHardwareReady && !seatHardwareReady + ? "CH1 → 车身 CAN FD / 主驾座椅 · CH0 与副驾 T2G 路径禁止" + : linkedHardwareReady ? "CH0 → 副驾座椅控制器 · CH1 → 车身 CAN FD" : seatHardwareReady ? `CH${hardware.channel} → 副驾座椅控制器 · 车身控制暂用虚拟 CAN` : "尚未配置可执行的物理 CAN 通道"; - $("#autoFlashNote").textContent = hardware.auto_flash_enabled - ? "未检测到匹配固件时,将在发送座椅控制帧前通过 IAR C-SPY 自动烧录并重新验证。" - : "仅接受周期反馈与四轴量程签名匹配的统一固件;需要自动烧录时请启用服务端开关。"; $("#hardwareBadge").className = `status-badge ${hardwareExecutionReady ? "" : "danger"}`.trim(); - $("#hardwareBadge").innerHTML = linkedHardwareReady + $("#hardwareBadge").innerHTML = ch1DriverOnly && vehicleHardwareReady && !seatHardwareReady + ? 'CH1 车端已配置' + : linkedHardwareReady ? '双通道已配置' : seatHardwareReady ? '座椅台架已配置' : '硬件锁定'; - $("#hardwareCoverage").textContent = linkedHardwareReady - ? `CH0 座椅 + CH${vehicleCan.channel} 车身 · ${approvalText}${accessText}${concurrencyText}` + $("#hardwareCoverage").textContent = ch1DriverOnly && vehicleHardwareReady && !seatHardwareReady + ? `CH1 主驾 + 车端设备 · 物理字段回读${accessText}${concurrencyText}` + : linkedHardwareReady + ? `CH0 座椅 + CH${vehicleCan.channel} 车身${accessText}${concurrencyText}` : seatHardwareReady ? `CH${hardware.channel} 座椅 · 物理反馈${accessText}${concurrencyText}` : "已阻断"; @@ -3795,6 +3917,9 @@ async function loadRuntime() { } async function synchronizePhysicalSeatState() { + if (state.runtime?.vehicle_can?.target_policy === "ch1_body_can_driver_only") { + return false; + } if (!state.runtime?.hardware?.configured) return false; const query = new URLSearchParams({ conversation_id: state.conversationId, @@ -4053,11 +4178,26 @@ function renderSeatState(actuatorState) { } const item = $(`.seat-card[data-seat="${seat}"] [data-function="${feature}"]`); if (!item) return; + if (feature === "massage") { + const mode = String(value.mode || "off"); + const strength = String(value.strength || "off"); + const active = !["off", "unknown", ""].includes(mode) + || !["off", "unknown", ""].includes(strength); + item.classList.toggle("on", active); + $("b", item).textContent = active + ? `${mode.toUpperCase()} · ${strength.toUpperCase()}` + : "OFF"; + return; + } item.classList.toggle("on", value.state === "on"); $("b", item).textContent = value.state === "on" ? `ON · L${value.level}` : "OFF"; }); }); - updateSeatTwin(mergedActuatorState); + const twinActuatorState = state.runtime?.vehicle_can?.target_policy === "ch1_body_can_driver_only" + && mergedActuatorState.driver + ? { ...mergedActuatorState, passenger: mergedActuatorState.driver } + : mergedActuatorState; + updateSeatTwin(twinActuatorState); } function renderDomainState(domainState) { @@ -4103,7 +4243,11 @@ function renderDomainState(domainState) { const vehicleSystems = state.seatTwin?.vehicleSystems; if (vehicleSystems) { vehicleSystems.targets.climateLevel = Math.max(0, Math.min(7, Number(climate.fan_level) || 0)); - vehicleSystems.targets.climateTemperature = Number(passengerTemperature) || 24; + vehicleSystems.targets.climateTemperature = Number( + state.runtime?.vehicle_can?.target_policy === "ch1_body_can_driver_only" + ? driverTemperature + : passengerTemperature, + ) || 24; } } @@ -4130,10 +4274,19 @@ function renderDomainState(domainState) { const ambient = domainState.ambient_light; if (ambient) { const active = ambient.power === "on"; - const color = /^#[0-9A-Fa-f]{6}$/.test(ambient.color_hex || "") + const indexedPhysicalColor = ambient.feedback_source === "physical_can_ch1" + && Number.isInteger(ambient.front_color_index); + const color = indexedPhysicalColor + ? "#D6DCE0" + : /^#[0-9A-Fa-f]{6}$/.test(ambient.color_hex || "") ? ambient.color_hex.toUpperCase() : "#74D9FF"; - const brightness = Math.max(0, Math.min(100, Number(ambient.brightness_pct) || 0)); + const physicalBrightness = Number(ambient.front_brightness_level); + const brightness = Math.max(0, Math.min(100, + Number.isFinite(physicalBrightness) && ambient.feedback_source === "physical_can_ch1" + ? physicalBrightness * 20 + : Number(ambient.brightness_pct) || 0, + )); const opacity = active ? Math.max(0.18, brightness / 100) : 0.08; const modeLabels = { camping: "露营", @@ -4145,14 +4298,18 @@ function renderDomainState(domainState) { on: "开启", off: "关闭", }; - $('[data-domain-panel="ambient_light"]').classList.toggle("active", active); - $("#ambientPower").textContent = active ? "ON" : "OFF"; + const lightPanel = $('[data-domain-panel="ambient_light"]'); + lightPanel.dataset.ambientPower = ambient.power || "unknown"; $$(".ambient-line").forEach((line) => { line.style.background = color; line.style.boxShadow = `0 0 ${Math.round(5 + brightness / 8)}px ${color}`; line.style.opacity = String(opacity); }); - $("#ambientMode").textContent = modeLabels[ambient.mode] || ambient.mode || "自定义"; + $("#ambientMode").textContent = indexedPhysicalColor + ? (active ? "开启(车端色号)" : "关闭") + : modeLabels[ambient.mode] || ambient.mode || "自定义"; + $("#ambientBrightness").textContent = `${ambient.front_brightness_level ?? "-"} / ${ambient.rear_brightness_level ?? "-"}`; + $("#ambientColorIndex").textContent = `${ambient.front_color_index ?? "-"} / ${ambient.rear_color_index ?? "-"}`; const vehicleSystems = state.seatTwin?.vehicleSystems; if (vehicleSystems) { vehicleSystems.targets.ambientPower = active ? 1 : 0; @@ -4161,6 +4318,22 @@ function renderDomainState(domainState) { } } + const interiorLight = domainState.interior_light; + if (interiorLight) { + const lightPanel = $('[data-domain-panel="ambient_light"]'); + lightPanel.dataset.interiorPower = interiorLight.power || "unknown"; + $("#interiorLightPower").textContent = interiorLight.power === "on" ? "开启" : "关闭"; + } + if (ambient || interiorLight) { + const lightPanel = $('[data-domain-panel="ambient_light"]'); + const ambientOn = lightPanel.dataset.ambientPower === "on"; + const interiorOn = lightPanel.dataset.interiorPower === "on"; + lightPanel.classList.toggle("active", ambientOn || interiorOn); + $("#ambientPower").textContent = ambientOn + ? "AMBIENT" + : interiorOn ? "INTERIOR" : "OFF"; + } + const windowState = domainState.window || domainState.windows; if (windowState) { const readWindowPosition = (key) => { @@ -4290,7 +4463,8 @@ const CONFIG_CAPABILITY_LABELS = { "climate.set_outlet_mode": "设置出风模式", "climate.set_front_defrost": "设置前风挡除霜", "climate.set_control_mode": "设置空调控制模式", - "door.set_rear_lock": "设置后排门锁", + "door.set_vehicle_lock": "整车门锁(当前 CH1 仅观测)", + "door.set_rear_lock": "二排门锁(当前 CH1 仅观测、已停用)", "window.set_motion": "控制车窗动作", "window.set_position": "设置车窗开度", "outlet.set_position_percent": "设置电动风口位置", @@ -4602,7 +4776,9 @@ function renderExecution(result) { : simulatedHil ? `${executedDomains.join(" / ")} 已完成隔离 HIL 仿真,未访问物理 CAN` : physical - ? `${executedDomains.join(" / ")} 已下发真实 CAN,并以 IAR 固件实际反馈确认 ${operationResults.filter((item) => item.status === "succeeded").length} 个步骤` + ? state.runtime?.vehicle_can?.target_policy === "ch1_body_can_driver_only" + ? `${executedDomains.join(" / ")} 已通过 CH1 下发,并以车端状态字段确认 ${operationResults.filter((item) => item.status === "succeeded").length} 个步骤` + : `${executedDomains.join(" / ")} 已下发真实 CAN,并以 IAR 固件实际反馈确认 ${operationResults.filter((item) => item.status === "succeeded").length} 个步骤` : `${executedDomains.join(" / ")} 共 ${report.spec.results.length} 个操作已完成 CAN 编码与虚拟执行`; if (partiallySucceeded) { executionDetail = `部分执行完成:${succeededDomains.join(" / ")} 成功 ${succeededOperations.length} 项;${failedDomains.join(" / ")} 未完成 ${failedOperations.length} 项,失败设备未阻断其他设备`; @@ -4682,12 +4858,67 @@ function renderExecution(result) { refreshIcons(); } +function physicalCh1StatePatch(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + if (value.source === "physical_can_ch1" || value.feedback_source === "physical_can_ch1") { + return value; + } + const patch = {}; + Object.entries(value).forEach(([key, item]) => { + const child = physicalCh1StatePatch(item); + if (child) patch[key] = child; + }); + return Object.keys(patch).length ? patch : null; +} + +function isAuthoritativeVehicleStatePayload(payload) { + if (!payload || typeof payload !== "object") return false; + if (payload.conversation_id !== state.conversationId) return false; + return Boolean( + physicalCh1StatePatch(payload.actuator_state) + || physicalCh1StatePatch(payload.domain_state), + ); +} + +function applyAuthoritativeVehicleState(payload) { + if (!isAuthoritativeVehicleStatePayload(payload)) return false; + const turn = Number(payload.conversation_turn); + if (Number.isFinite(turn) && turn < state.vehicleStateTurn) return false; + if (Number.isFinite(turn)) state.vehicleStateTurn = turn; + const actuatorPatch = physicalCh1StatePatch(payload.actuator_state); + const domainPatch = physicalCh1StatePatch(payload.domain_state); + const previousActuatorState = state.vehicleStateSnapshot?.actuatorState || null; + state.vehicleStateSnapshot = { + conversationId: payload.conversation_id, + conversationTurn: Number.isFinite(turn) ? turn : null, + requestId: payload.request_id || null, + actuatorState: actuatorPatch + ? mergeSeatActuatorState(previousActuatorState, actuatorPatch) + : previousActuatorState, + domainState: domainPatch, + }; + if (actuatorPatch?.driver) { + state.seatFeedbackSource = "physical_can_ch1"; + } + renderSeatState(actuatorPatch); + renderDomainState(domainPatch); + updateSeatFeedbackModeLabel(); + return true; +} + +window.addEventListener("cognitive-agent:extension-vehicle-result", (event) => { + applyAuthoritativeVehicleState(event.detail); +}); + function renderResult(result) { state.result = result; const reportMode = result.execution_report?.spec?.mode; const feedbackSource = result.execution_report?.metadata?.labels?.seat_feedback; if (feedbackSource === "simulated_hardware") { state.seatFeedbackSource = "simulated_hardware"; + } else if (feedbackSource === "physical_can_ch1" + && isAuthoritativeVehicleStatePayload(result)) { + state.seatFeedbackSource = "physical_can_ch1"; } else if (reportMode === "hil") { state.seatFeedbackSource = result.execution_report?.spec?.physical_state?.complete ? "physical_can" @@ -4726,9 +4957,15 @@ function renderResult(result) { renderDecision(result); renderTrace(result.trace_events || []); renderValidation(result.validation || []); - renderSeatState(result.actuator_state); - updateSeatFeedbackModeLabel(); - renderDomainState(result.domain_state); + const ch1HardwareResult = reportMode === "hil" + && state.runtime?.vehicle_can?.target_policy === "ch1_body_can_driver_only"; + if (ch1HardwareResult) { + applyAuthoritativeVehicleState(result); + } else { + renderSeatState(result.actuator_state); + renderDomainState(result.domain_state); + updateSeatFeedbackModeLabel(); + } updateTwinExplicitAngles(result); renderSavedPresets(result); if (result.mode_profile) loadRuntime(); @@ -4744,7 +4981,9 @@ async function submitRequest(event) { showToast("请输入车舱控制请求"); return; } + const driverOnlyCh1 = isDriverOnlyCh1Runtime(); if (state.executionMode === "hardware" + && !driverOnlyCh1 && !isLocalDecisionHost() && state.hardwareAccess?.state !== "approved") { showToast(state.hardwareAccess?.state === "pending" @@ -4760,14 +4999,6 @@ async function submitRequest(event) { setRunStatus("processing", "PROCESSING"); $("#resultTitle").textContent = "正在执行车舱编排"; startProgress(); - const approvalTokenInput = $("#approvalToken"); - const approvalRequired = state.executionMode === "hardware" - && Boolean(state.runtime?.hardware?.approval_required); - const approvalToken = approvalRequired - ? approvalTokenInput?.value || "" - : null; - if (approvalTokenInput) approvalTokenInput.value = ""; - try { const response = await apiRequest("/api/requests", { method: "POST", @@ -4779,15 +5010,12 @@ async function submitRequest(event) { conversation_id: state.conversationId, vehicle_id: state.vehicleId || state.runtime?.default_vehicle_id, execution_mode: state.executionMode, - ...(state.executionMode === "hardware" + ...(state.executionMode === "hardware" && !driverOnlyCh1 ? { hardware_client_id: state.hardwareClientId } : {}), ...(state.executionMode === "simulated_hardware" ? { simulation_fault: $("#simulationFault").value } : {}), - ...(approvalRequired - ? { approval_token: approvalToken } - : {}), }), }); const payload = await response.json(); @@ -4839,11 +5067,8 @@ async function submitRequest(event) { } finally { button.disabled = false; button.classList.remove("loading"); - const hardwareLabel = state.runtime?.hardware?.approval_required - ? "授权并真实执行" - : "真实执行"; const buttonLabel = state.executionMode === "hardware" - ? hardwareLabel + ? "真实执行" : state.executionMode === "simulated_hardware" ? "运行故障仿真" : "生成并执行"; button.innerHTML = `${buttonLabel}`; refreshIcons(); @@ -4923,6 +5148,9 @@ window.CognitiveAgentConsole = { getExecutionMode() { return state.executionMode; }, + getConversationId() { + return state.conversationId; + }, getResult() { return state.result; }, @@ -4982,11 +5210,13 @@ document.addEventListener("DOMContentLoaded", async () => { $$('[data-download]').forEach((button) => button.addEventListener("click", () => downloadArtifact(button.dataset.download))); await loadAuthStatus(); await loadRuntime(); - await loadHardwareAccessStatus({ silent: true }); - state.hardwareAccessPoller = window.setInterval( - () => loadHardwareAccessStatus({ silent: true }), - 3000, - ); + if (!isDriverOnlyCh1Runtime()) { + await loadHardwareAccessStatus({ silent: true }); + state.hardwareAccessPoller = window.setInterval( + () => loadHardwareAccessStatus({ silent: true }), + 3000, + ); + } await synchronizePhysicalSeatState(); if (new URLSearchParams(window.location.search).get("autorun") === "1") { $("#requestForm").requestSubmit(); diff --git a/apps/web-console/console-v2.js b/apps/web-console/console-v2.js index 037ce82..9302d54 100644 --- a/apps/web-console/console-v2.js +++ b/apps/web-console/console-v2.js @@ -209,7 +209,6 @@ const providerField = $(".field-group:has(.segmented [data-provider])", originalForm); const executionField = $(".field-group:has(.execution-mode-options)", originalForm); const simulationFaultField = $("#simulationFaultField", originalForm); - const approvalField = $("#approvalField", originalForm); if (vehicleField) { vehicleField.classList.add("v2-config-field", "v2-vehicle-field"); @@ -280,7 +279,7 @@ } const advanced = make("div", "v2-advanced-stack", ""); - [simulationFaultField, approvalField].forEach((field) => { + [simulationFaultField].forEach((field) => { if (!field) return; field.classList.add("v2-config-field", "v2-advanced-field"); advanced.append(field); diff --git a/apps/web-console/index.html b/apps/web-console/index.html index 3463dce..66a7f1f 100755 --- a/apps/web-console/index.html +++ b/apps/web-console/index.html @@ -9,7 +9,7 @@ 智舱随行 · 整车智能控制台 - + @@ -19,11 +19,11 @@ - + - + @@ -139,13 +139,6 @@

正在读取座椅量程与角度标定

- -