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 @@
正在读取座椅量程与角度标定
-
-
-
-
仅在服务端启用令牌门禁时显示;令牌不会写入日志、会话或执行制品。
-
正在检查自动烧录配置
-
-