feat: synchronize CH1 vehicle control and feedback

This commit is contained in:
vee
2026-08-11 10:42:13 +08:00
parent 3701192a11
commit 32379a1573
50 changed files with 13636 additions and 736 deletions
+7
View File
@@ -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 标定协议以及
+288 -58
View File
@@ -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) {
? '<i data-lucide="bug-play"></i>SIM · 模拟台架'
: '<i data-lucide="flask-conical"></i>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 = `
<button type="button" data-mode-command="seat driver vent low"><i data-lucide="wind"></i>主驾通风</button>
<button type="button" data-mode-command="ac temp driver 24"><i data-lucide="snowflake"></i>主驾 24℃</button>
<button type="button" data-mode-command="shade position 0"><i data-lucide="panels-top-left"></i>遮阳帘关闭</button>`;
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 = '<i data-lucide="armchair"></i>主驾座椅 · 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
? '<i data-lucide="shield-check"></i>CH1 车端已配置'
: linkedHardwareReady
? '<i data-lucide="shield-check"></i>双通道已配置'
: seatHardwareReady
? '<i data-lucide="shield-check"></i>座椅台架已配置'
: '<i data-lucide="shield-off"></i>硬件锁定';
$("#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 = `<i data-lucide="play"></i><span>${buttonLabel}</span>`;
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();
+1 -2
View File
@@ -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);
+15 -18
View File
@@ -9,7 +9,7 @@
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" href="/icons/app-icon-192.png" />
<title>智舱随行 · 整车智能控制台</title>
<link rel="stylesheet" href="/styles.css?v=android-pad-4" />
<link rel="stylesheet" href="/styles.css?v=android-pad-5" />
<link rel="stylesheet" href="/console-v2.css?v=desktop-refactor-68" />
<link rel="stylesheet" href="/self-extension.css?v=extension-9" />
<link rel="stylesheet" href="/self-calibration.css?v=calibration-4" />
@@ -19,11 +19,11 @@
<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-1" defer></script>
<script src="/app.js?v=seat-hil-44" defer></script>
<script src="/app.js?v=seat-hil-51" defer></script>
<script type="module" src="/gesture-control.js?v=android-pad-6"></script>
<script src="/pwa.js?v=android-pad-1" defer></script>
<script src="/console-v2.js?v=desktop-refactor-28" defer></script>
<script src="/self-extension.js?v=extension-6" defer></script>
<script src="/self-extension.js?v=extension-15" defer></script>
<script src="/self-calibration.js?v=calibration-2" defer></script>
<script src="/self-repair.js?v=repair-17" defer></script>
<script src="/self-repair-seat3d.js?v=repair-17" defer></script>
@@ -139,13 +139,6 @@
<p class="provider-note" id="vehicleNote">正在读取座椅量程与角度标定</p>
</div>
<div class="field-group approval-field" id="approvalField" hidden>
<label class="field-label" for="approvalToken">硬件执行授权令牌</label>
<input id="approvalToken" name="approval_token" type="password" maxlength="128" autocomplete="off" spellcheck="false" placeholder="请输入服务端配置令牌" />
<p class="provider-note">仅在服务端启用令牌门禁时显示;令牌不会写入日志、会话或执行制品。</p>
<p class="provider-note" id="autoFlashNote">正在检查自动烧录配置</p>
</div>
<button class="primary-action" id="runButton" type="submit" aria-label="执行车舱控制指令">
<i data-lucide="play"></i>
<span>生成并执行</span>
@@ -236,7 +229,7 @@
<article class="domain-panel climate-panel" data-domain-panel="climate">
<header>
<span class="domain-icon"><i data-lucide="fan"></i></span>
<div><strong>空调</strong><small>CLIMATE · 0x064</small></div>
<div><strong>空调</strong><small>CMD 0x1DE · FB 0x404 · CH1</small></div>
<span class="device-state" id="climatePower">OFF</span>
</header>
<div class="climate-display">
@@ -269,7 +262,7 @@
<article class="domain-panel roof-panel" data-domain-panel="roof">
<header>
<span class="domain-icon"><i data-lucide="panels-top-left"></i></span>
<div><strong>智能天幕</strong><small>ROOF · 0x4C1 / 0x3E6</small></div>
<div><strong>遮阳帘</strong><small>CMD 0x431 · FB 0x41A · CH1</small></div>
<span class="device-state" id="roofState">READY</span>
</header>
<div class="roof-visual" aria-label="天幕透光率与遮阳帘位置">
@@ -287,10 +280,10 @@
<article class="domain-panel ambient-panel" data-domain-panel="ambient_light">
<header>
<span class="domain-icon"><i data-lucide="lamp-ceiling"></i></span>
<div><strong>阅读灯</strong><small>AMBIENT · RGB 16.8M</small></div>
<div><strong>车内灯光</strong><small>INTERIOR / AMBIENT · CH1</small></div>
<span class="device-state" id="ambientPower">OFF</span>
</header>
<div class="ambient-visual" id="ambientVisual" aria-label="副驾阅读灯状态">
<div class="ambient-visual" id="ambientVisual" aria-label="CH1 全车氛围灯状态">
<div class="ambient-dashboard-sketch" aria-hidden="true">
<span class="ambient-dashboard-top"></span>
<span class="ambient-center-screen"></span>
@@ -301,17 +294,20 @@
<i class="ambient-line"></i><i class="ambient-line"></i><i class="ambient-line"></i>
</span>
</div>
<small class="ambient-location-label">副驾 · 手套箱上方排灯</small>
<small class="ambient-location-label">全车 · CH1 氛围灯</small>
</div>
<dl class="domain-readouts single-readout">
<div><dt>模式</dt><dd id="ambientMode">关闭</dd></div>
<dl class="domain-readouts">
<div><dt>氛围灯</dt><dd id="ambientMode">关闭</dd></div>
<div><dt>室内/阅读灯</dt><dd id="interiorLightPower">关闭</dd></div>
<div><dt>氛围亮度</dt><dd id="ambientBrightness">0 / 0</dd></div>
<div><dt>氛围色号</dt><dd id="ambientColorIndex">- / -</dd></div>
</dl>
</article>
<article class="domain-panel window-panel" data-domain-panel="window">
<header>
<span class="domain-icon"><i data-lucide="gallery-vertical-end"></i></span>
<div><strong>车窗</strong><small>WINDOW · OPENING</small></div>
<div><strong>车窗</strong><small>CMD 0x431 · FB 0x388 · CH1</small></div>
<span class="device-state" id="windowState">READY</span>
</header>
<dl class="domain-readouts">
@@ -409,6 +405,7 @@
<div class="thermal-states">
<span data-function="ventilation"><i data-lucide="wind"></i>通风 <b>--</b></span>
<span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span>
<span data-function="massage"><i data-lucide="waves"></i>按摩 <b>--</b></span>
</div>
<div class="motion-state-grid" aria-live="polite"></div>
</article>
+397 -79
View File
@@ -3,10 +3,17 @@
const STORAGE_KEY = "cognitive-agent.ir-toy-console.v5";
const MODE_KEY = "cognitive-agent.extension-mode";
const CONVERSATION_KEY = "cognitive-agent.conversation-id";
const SLOT_MIN = 1;
const SLOT_MAX = 248;
const ACTION_PAGE_SIZE = 6;
const HISTORY_LIMIT = 40;
const MAX_PLAN_STEPS = 12;
const MIN_PLAN_DELAY_MS = 1;
const MAX_PLAN_DELAY_MS = 30000;
const MAX_TOTAL_PLAN_DELAY_MS = 60000;
const HEALTH_POLL_INTERVAL_MS = 3000;
const HEALTH_REQUEST_TIMEOUT_MS = 5000;
const $ = (selector, root = document) => root.querySelector(selector);
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
@@ -145,41 +152,45 @@
};
const VEHICLE_DEVICES = [
{ name: "座椅", code: "SEAT · CAN", icon: "armchair", actions: [
{ name: "主驾通风", command: "打开主驾座椅通风" },
{ name: "主驾加热", command: "打开主驾座椅加热" },
{ name: "副驾通风", command: "打开副驾座椅通风" },
{ name: "座椅位置", command: "把副驾座椅向后调一点" },
{ name: "主驾座椅", code: "SEAT · CH1 BODY CAN", icon: "armchair", actions: [
{ name: "通风低档", command: "seat driver vent low" },
{ name: "通风高档", command: "seat driver vent high" },
{ name: "关闭通风", command: "seat driver vent off" },
{ name: "加热低档", command: "seat driver heat low" },
{ name: "加热高档", command: "seat driver heat high" },
{ name: "关闭加热", command: "seat driver heat off" },
{ name: "按摩弱档", command: "seat driver massage strength weak" },
{ name: "舒缓按摩", command: "seat driver massage mode relax" },
{ name: "关闭按摩", command: "seat driver massage off" },
] },
{ name: "空调", code: "CLIMATE · CAN", icon: "snowflake", actions: [
{ name: "开启空调", command: "打开空调" },
{ name: "24℃", command: "把空调温度调到24度" },
{ name: "风量 3", command: "把空调风量调到3档" },
{ name: "自动模式", command: "把空调设置为自动模式" },
] },
{ name: "电动出风口", code: "OUTLET · 4 ZONES", icon: "wind", actions: [
{ name: "主驾左", command: "把主驾左出风口调到中间位置" },
{ name: "主驾右", command: "把主驾右出风口调到中间位置" },
{ name: "副驾左", command: "把副驾左出风口调到中间位置" },
{ name: "副驾右", command: "把副驾右出风口调到中间位置" },
{ name: "开启空调", command: "ac on" },
{ name: "关闭空调", command: "ac off" },
{ name: "24℃", command: "ac temp driver 24" },
{ name: "风量 3", command: "ac fan 3" },
{ name: "除霜出风", command: "ac mode defrost" },
{ name: "自动模式", command: "ac control auto" },
] },
{ name: "遮阳帘", code: "ROOF · SHADE", icon: "panels-top-left", actions: [
{ name: "打开", command: "打开遮阳帘" },
{ name: "关闭", command: "关闭遮阳帘" },
{ name: "半开", command: "把遮阳帘打开一半" },
{ name: "停止", command: "停止遮阳帘" },
{ name: "开启 10%", command: "shade position 10" },
{ name: "打开", command: "shade position 100" },
{ name: "关闭", command: "shade position 0" },
{ name: "半开", command: "shade position 50" },
] },
{ name: "车内灯光", code: "INTERIOR · LIGHT", icon: "lamp-ceiling", actions: [
{ name: "室内灯", command: "打开车内室内灯" },
{ name: "阅读灯", command: "打开全部阅读灯" },
{ name: "氛围灯", command: "打开车内氛围灯" },
{ name: "氛围亮度", command: "把氛围灯亮度调到3档" },
{ name: "室内灯", command: "light indoor on" },
{ name: "关闭室内灯", command: "light indoor off" },
{ name: "阅读灯", command: "light reading all on" },
{ name: "关闭阅读灯", command: "light reading all off" },
{ name: "氛围灯", command: "light ambient on" },
{ name: "关闭氛围灯", command: "light ambient off" },
{ name: "氛围色号 32", command: "light ambient color 32" },
{ name: "氛围亮度", command: "light ambient brightness 3" },
] },
{ name: "车窗与门锁", code: "WINDOW · DOOR", icon: "gallery-vertical-end", actions: [
{ name: "全部开窗", command: "打开全部车窗" },
{ name: "全部关窗", command: "关闭全部车窗" },
{ name: "左前窗 50%", command: "把左前车窗打开到50%" },
{ name: "后排门锁", command: "锁上后排门锁" },
{ name: "车窗", code: "WINDOW · CH1", icon: "gallery-vertical-end", actions: [
{ name: "左前窗 10%", command: "window front-left position 10" },
{ name: "左前窗一半", command: "window front-left position 50" },
{ name: "左前窗关闭", command: "window front-left position 0" },
] },
];
@@ -199,6 +210,10 @@
let isPlanning = false;
let isExecuting = false;
let healthState = null;
let healthRefreshPromise = null;
let healthPollTimer = null;
let healthToastRequested = false;
let latestVehicleStateTurn = -1;
function uid(prefix) {
if (globalThis.crypto?.randomUUID) return `${prefix}-${crypto.randomUUID()}`;
@@ -402,6 +417,9 @@
<div class="v2-extension-vehicle-grid" id="extensionVehicleGrid"></div>
</section>
</div>
<div class="v2-extension-run-plan" id="extensionVehicleStateSummary" data-state="unknown">
<span><i>CH1</i>等待权威车端状态</span>
</div>
<div class="v2-extension-run-plan" id="extensionRunPlan" hidden></div>
</section>`;
moduleSwitcher.insertAdjacentElement("afterend", root);
@@ -489,11 +507,106 @@
}
const payload = await response.json().catch(() => ({}));
if (!response.ok || payload.success === false) {
throw new Error(payload.message || `自扩展请求失败(HTTP ${response.status})`);
const error = new Error(payload.message || `自扩展请求失败(HTTP ${response.status})`);
error.payload = payload;
throw error;
}
return payload;
}
function sharedConversationId() {
const fromConsole = window.CognitiveAgentConsole?.getConversationId?.();
if (typeof fromConsole === "string" && fromConsole.startsWith("conv-")) {
sessionStorage.setItem(CONVERSATION_KEY, fromConsole);
return fromConsole;
}
const stored = sessionStorage.getItem(CONVERSATION_KEY);
if (stored?.startsWith("conv-")) return stored;
const conversationId = uid("conv");
sessionStorage.setItem(CONVERSATION_KEY, conversationId);
return conversationId;
}
function containsAuthoritativePhysicalCh1State(value) {
if (!value || typeof value !== "object") return false;
if (value.source === "physical_can_ch1" || value.feedback_source === "physical_can_ch1") {
return true;
}
return Object.values(value).some((item) => containsAuthoritativePhysicalCh1State(item));
}
function isAuthoritativeVehicleStatePayload(payload) {
if (!payload || typeof payload !== "object") return false;
if (payload.conversation_id !== sharedConversationId()) return false;
return containsAuthoritativePhysicalCh1State(payload.actuator_state)
|| containsAuthoritativePhysicalCh1State(payload.domain_state);
}
function renderAuthoritativeVehicleState(payload) {
if (!isAuthoritativeVehicleStatePayload(payload)) return false;
const turn = Number(payload.conversation_turn);
if (Number.isFinite(turn) && turn < latestVehicleStateTurn) return false;
if (Number.isFinite(turn)) latestVehicleStateTurn = turn;
const summary = $("#extensionVehicleStateSummary");
if (!summary) return false;
const requestLabel = String(payload.request_id || "-").slice(-8);
const items = [
`<span class="done"><i>CH1</i>真实车况 · TURN ${Number.isFinite(turn) ? turn : "-"} · ${escapeHtml(requestLabel)}</span>`,
];
const feedback = Array.isArray(payload.physical_feedback)
? payload.physical_feedback.filter((item) => (
item?.confirmed === true
&& item?.channel === 1
&& item?.source === "physical_can_ch1"
))
: [];
if (feedback.length) {
const frames = [...new Set(feedback.map((item) => item.frame?.frame_id).filter(Boolean))];
items.push(`<span><i>FB</i>${feedback.length}/${payload.physical_feedback.length} MATCH · ${escapeHtml(frames.join(" / ") || "CH1")}</span>`);
}
const driver = payload.actuator_state?.driver || {};
const ventilation = driver.ventilation;
const heating = driver.heating;
if (ventilation?.feedback_source === "physical_can_ch1") {
items.push(`<span><i>VENT</i>${ventilation.state === "on" ? `ON L${escapeHtml(ventilation.level)}` : "OFF"}</span>`);
}
if (heating?.feedback_source === "physical_can_ch1") {
items.push(`<span><i>HEAT</i>${heating.state === "on" ? `ON L${escapeHtml(heating.level)}` : "OFF"}</span>`);
}
const massage = driver.massage;
if (massage?.feedback_source === "physical_can_ch1") {
items.push(`<span><i>MASSAGE</i>${escapeHtml(massage.mode || "off")} · ${escapeHtml(massage.strength || "off")}</span>`);
}
const climate = payload.domain_state?.climate;
if (climate?.feedback_source === "physical_can_ch1") {
const temperature = climate.zones?.driver?.temperature_c ?? "-";
const outletMode = climate.zones?.driver?.outlet_mode || "-";
items.push(`<span><i>AC</i>${escapeHtml(climate.power || climate.ac || "unknown")} · ${escapeHtml(temperature)}℃ · FAN ${escapeHtml(climate.fan_level ?? "-")} · ${escapeHtml(outletMode)}</span>`);
}
const roof = payload.domain_state?.roof;
if (roof?.feedback_source === "physical_can_ch1") {
items.push(`<span><i>ROOF</i>${escapeHtml(roof.shade_position_pct ?? "-")}% · ${escapeHtml(roof.motion_state || "unknown")}</span>`);
}
const frontLeftWindow = payload.domain_state?.window?.positions?.front_left;
if (frontLeftWindow?.feedback_source === "physical_can_ch1") {
items.push(`<span><i>WINDOW</i>左前 ${escapeHtml(frontLeftWindow.position_pct ?? "-")}%</span>`);
}
const interiorLight = payload.domain_state?.interior_light;
if (interiorLight?.feedback_source === "physical_can_ch1") {
items.push(`<span><i>INTERIOR</i>${escapeHtml(interiorLight.power || "unknown")}</span>`);
}
const ambientLight = payload.domain_state?.ambient_light;
if (ambientLight?.feedback_source === "physical_can_ch1") {
const brightness = `${ambientLight.front_brightness_level ?? "-"}/${ambientLight.rear_brightness_level ?? "-"}`;
const color = `${ambientLight.front_color_index ?? "-"}/${ambientLight.rear_color_index ?? "-"}`;
items.push(`<span><i>AMBIENT</i>${escapeHtml(ambientLight.power || "unknown")} · L${escapeHtml(brightness)} · C${escapeHtml(color)}</span>`);
}
summary.dataset.state = "physical_can_ch1";
summary.innerHTML = items.join("");
return true;
}
function setMode(mode) {
activeMode = mode === "control" ? "control" : "study";
sessionStorage.setItem(MODE_KEY, activeMode);
@@ -864,20 +977,51 @@
}
function hydratePlan(steps) {
return (steps || []).map((step) => {
if (!Array.isArray(steps)) throw new Error("联动计划步骤格式无效。");
if (steps.length > MAX_PLAN_STEPS) throw new Error("联动计划步骤过多。");
const plan = steps.map((step) => {
if (!step || typeof step !== "object" || Array.isArray(step)) {
throw new Error("联动计划包含无效步骤。");
}
if (step.kind === "delay") {
if (!Number.isInteger(step.delay_ms)
|| step.delay_ms < MIN_PLAN_DELAY_MS
|| step.delay_ms > MAX_PLAN_DELAY_MS) {
throw new Error("延时步骤必须使用安全范围内的整数毫秒。");
}
if ([step.capability_id, step.command].some(
(value) => value != null && String(value).trim() !== "",
)) {
throw new Error("延时步骤不能包含设备命令。");
}
return {
kind: "delay",
delayMs: step.delay_ms,
label: step.delay_ms % 1000 === 0
? `等待 ${step.delay_ms / 1000} 秒`
: `等待 ${step.delay_ms} 毫秒`,
risk: "low",
};
}
if (step.delay_ms !== undefined && step.delay_ms !== 0) {
throw new Error("非延时步骤不能携带延时值。");
}
if (step.kind === "vehicle") {
const command = String(step.command || "").trim();
if (!command) throw new Error("车端计划步骤缺少命令。");
return {
kind: "vehicle",
command: step.command,
label: step.label || step.command,
command,
label: step.label || command,
risk: step.risk || "medium",
};
}
if (step.kind !== "peripheral") throw new Error("联动计划包含未知步骤。");
const capability = state.capabilities.find((item) => item.id === step.capability_id);
const device = state.devices.find((item) => item.id === capability?.deviceId);
if (!capability || !device || !learnedCapabilities(device.id).some(
(item) => item.id === capability.id,
)) return null;
)) throw new Error("联动计划引用了尚未学习的外设能力。");
return {
kind: "peripheral",
device,
@@ -885,7 +1029,22 @@
label: `${device.name} · ${capability.name}`,
risk: capability.risk || "low",
};
}).filter(Boolean);
});
let totalDelayMs = 0;
plan.forEach((step, index) => {
if (step.kind !== "delay") return;
if (index === 0 || index === plan.length - 1) {
throw new Error("延时步骤必须位于两个动作之间。");
}
if (plan[index - 1].kind === "delay") {
throw new Error("联动计划不能包含连续延时。");
}
totalDelayMs += step.delayMs;
});
if (totalDelayMs > MAX_TOTAL_PLAN_DELAY_MS) {
throw new Error("联动计划累计延时超过安全上限。");
}
return plan;
}
function setExecutionState(stateName, title, note, progress = 0) {
@@ -955,11 +1114,92 @@
}
}
async function executeVehicle(command) {
return api("vehicle/execute", {
method: "POST",
body: JSON.stringify({ command, vehicle_id: "denza_n7" }),
function vehicleExecutionSucceeded(result) {
const operations = Array.isArray(result?.operation_results)
? result.operation_results
: [];
const everyOperationSucceeded = operations.length > 0
&& operations.every((item) => item?.status === "succeeded");
const hardwareRequest = result?.execution_mode === "hardware";
const feedback = Array.isArray(result?.physical_feedback)
? result.physical_feedback
: [];
const everyHardwareOperationConfirmed = !hardwareRequest || (
result?.physical_feedback_confirmed === true
&& feedback.length === operations.length
&& feedback.every((item) => item?.confirmed === true)
);
return result?.success === true
&& result?.status === "succeeded"
&& everyOperationSucceeded
&& everyHardwareOperationConfirmed;
}
function synchronizeVehicleExecution(command, result) {
if (!result || typeof result !== "object") return;
const completed = vehicleExecutionSucceeded(result);
state.lastVehicleExecution = {
at: new Date().toISOString(),
command,
success: completed,
status: result.status || "unknown",
executionMode: result.execution_mode || null,
requestId: result.request_id || null,
operationResults: result.operation_results || [],
executionReport: result.execution_report || null,
domainState: result.domain_state || null,
actuatorState: result.actuator_state || null,
physicalState: result.physical_state || null,
physicalFeedback: result.physical_feedback || [],
physicalFeedbackConfirmed: result.physical_feedback_confirmed === true,
conversationId: result.conversation_id || null,
conversationTurn: result.conversation_turn ?? null,
};
saveState();
renderAuthoritativeVehicleState(result);
$$('[data-extension-vehicle-command]').forEach((button) => {
if (button.dataset.extensionVehicleCommand !== command) return;
button.dataset.executionState = completed ? "succeeded" : "failed";
button.setAttribute(
"aria-label",
`${button.textContent.trim()}:${completed ? "执行成功" : "执行未完成"}`,
);
button.title = `${command} · ${result.message || result.status || "执行结果未知"}`;
});
window.dispatchEvent(new CustomEvent("cognitive-agent:extension-vehicle-result", {
detail: result,
}));
}
function vehicleExecutionError(result) {
const failed = (result?.operation_results || []).find(
(item) => item?.status !== "succeeded",
);
return result?.message
|| failed?.message
|| `车端动作未成功完成(${result?.status || "unknown"})`;
}
async function executeVehicle(command) {
let result;
try {
result = await api("vehicle/execute", {
method: "POST",
// Channel, seat target, provider, vehicle profile and execution mode
// are intentionally absent: those are server-side CH1 safety policy.
body: JSON.stringify({ command, conversation_id: sharedConversationId() }),
});
} catch (error) {
if (error.payload) synchronizeVehicleExecution(command, error.payload);
throw error;
}
synchronizeVehicleExecution(command, result);
if (!vehicleExecutionSucceeded(result)) {
const error = new Error(vehicleExecutionError(result));
error.payload = result;
throw error;
}
return result;
}
async function executePlan(plan) {
@@ -968,6 +1208,7 @@
renderRunPlan(plan);
let completed = 0;
let vehicleDryRun = false;
let vehicleHardwareFeedback = 0;
for (let index = 0; index < plan.length; index += 1) {
const step = plan[index];
renderRunPlan(plan, index);
@@ -978,9 +1219,14 @@
(index / plan.length) * 100,
);
try {
if (step.kind === "vehicle") {
if (step.kind === "delay") {
await delay(step.delayMs);
} else if (step.kind === "vehicle") {
const result = await executeVehicle(step.command);
vehicleDryRun ||= result.execution_mode === "dry_run";
if (result.execution_mode === "hardware") {
vehicleHardwareFeedback += result.physical_feedback.length;
}
} else {
await sendGatewayCommand("emit", { slot: step.capability.slot });
}
@@ -992,14 +1238,21 @@
showToast(error.message, "error");
break;
}
if (index < plan.length - 1) await delay(state.settings.actionDelayMs);
const nextStep = plan[index + 1];
if (nextStep && step.kind !== "delay" && nextStep.kind !== "delay") {
await delay(state.settings.actionDelayMs);
}
}
renderRunPlan(plan, completed);
if (completed === plan.length) {
setExecutionState(
"success",
"联动计划已全部处理",
vehicleDryRun ? "外设已发射,车端处于安全演练模式" : "外设与车端链路处理完成",
vehicleDryRun
? "外设已发射,车端处于安全演练模式"
: vehicleHardwareFeedback > 0
? `车端 ${vehicleHardwareFeedback} 项动作均已同步物理反馈`
: "外设与车端链路处理完成",
100,
);
showToast("联动计划已全部处理。", "success");
@@ -1038,46 +1291,97 @@
}]);
}
async function refreshHealth(showMessage = false) {
$("#extensionStudyHealth").dataset.state = "checking";
$("#extensionStudyHealthState").textContent = "CHECK";
$("#extensionVehicleCard").dataset.state = "checking";
$("#extensionVehicleState").textContent = "CHECK";
async function requestHealth() {
const controller = new AbortController();
const timeoutId = window.setTimeout(() => controller.abort(), HEALTH_REQUEST_TIMEOUT_MS);
try {
healthState = await api("health");
} catch (error) {
healthState = {
connected: false,
transport: "unavailable",
message: error.message,
gpt: { configured: false },
vehicle: { connected: false },
};
return await api("health", { signal: controller.signal });
} finally {
window.clearTimeout(timeoutId);
}
const connected = Boolean(healthState.connected);
const transport = String(healthState.transport || "--").toUpperCase();
const connectionLabel = connected
? (healthState.transport === "simulation" ? "模拟学习器已就绪" : `${transport} 学习器已连接`)
: "GSCU1B 尚未连接";
$("#extensionStudyHealthText").textContent = connectionLabel;
$("#extensionStudyHealthState").textContent = connected ? "READY" : "OFFLINE";
$("#extensionStudyHealth").dataset.state = connected ? "online" : "offline";
const vehicleConnected = Boolean(healthState.vehicle?.connected);
$("#extensionVehicleState").textContent = vehicleConnected ? "READY" : "OFFLINE";
$("#extensionVehicleCard").dataset.state = vehicleConnected ? "online" : "offline";
$("#extensionConnectionText").textContent = connectionLabel;
$(".v2-extension-footer").classList.toggle("offline", !connected);
$("#extensionModelName").textContent = healthState.gpt?.configured
? healthState.gpt.model || "GPT"
: "GPT 未配置";
$("#extensionModelState").textContent = healthState.gpt?.configured ? "READY" : "LOCAL";
$("#extensionModelCard").classList.toggle("unavailable", !healthState.gpt?.configured);
if (showMessage) {
const ready = [connected, healthState.gpt?.configured, healthState.vehicle?.connected]
.filter(Boolean).length;
showToast(`自扩展链路:${ready} / 3 项就绪。`, ready === 3 ? "success" : "error");
}
return healthState;
}
function refreshHealth(showMessage = false) {
healthToastRequested ||= showMessage;
if (healthRefreshPromise) return healthRefreshPromise;
healthRefreshPromise = (async () => {
if (!healthState || healthToastRequested) {
$("#extensionStudyHealth").dataset.state = "checking";
$("#extensionStudyHealthState").textContent = "CHECK";
$("#extensionVehicleCard").dataset.state = "checking";
$("#extensionVehicleState").textContent = "CHECK";
}
try {
healthState = await requestHealth();
} catch (error) {
healthState = {
connected: false,
listening: false,
transport: "unavailable",
message: error.message,
gpt: { configured: false },
vehicle: { connected: false },
};
}
const connected = Boolean(healthState.connected);
const listening = !connected && Boolean(healthState.listening);
const transport = String(healthState.transport || "--").toUpperCase();
let connectionLabel;
if (connected) {
const peer = String(healthState.peer || "").trim();
connectionLabel = healthState.transport === "simulation"
? "模拟学习器已就绪"
: `${transport} 学习器已连接${peer ? ` · ${peer}` : ""}`;
} else if (listening) {
connectionLabel = `${transport} 监听已就绪,等待 GSCU1B 接入`;
} else {
connectionLabel = healthState.message || "GSCU1B 尚未连接";
}
const studyHealthText = $("#extensionStudyHealthText");
studyHealthText.textContent = connectionLabel;
studyHealthText.title = healthState.message || connectionLabel;
$("#extensionStudyHealthState").textContent = connected
? "READY"
: (listening ? "WAIT" : "OFFLINE");
$("#extensionStudyHealth").dataset.state = connected
? "online"
: (listening ? "checking" : "offline");
const vehicleConnected = Boolean(healthState.vehicle?.connected);
$("#extensionVehicleState").textContent = vehicleConnected ? "READY" : "OFFLINE";
$("#extensionVehicleCard").dataset.state = vehicleConnected ? "online" : "offline";
const connectionText = $("#extensionConnectionText");
connectionText.textContent = connectionLabel;
connectionText.title = healthState.message || connectionLabel;
$(".v2-extension-footer").classList.toggle("offline", !connected);
$("#extensionModelName").textContent = healthState.gpt?.configured
? healthState.gpt.model || "GPT"
: "GPT 未配置";
$("#extensionModelState").textContent = healthState.gpt?.configured ? "READY" : "LOCAL";
$("#extensionModelCard").classList.toggle("unavailable", !healthState.gpt?.configured);
if (healthToastRequested) {
const ready = [connected, healthState.gpt?.configured, healthState.vehicle?.connected]
.filter(Boolean).length;
showToast(`自扩展链路:${ready} / 3 项就绪。`, ready === 3 ? "success" : "error");
}
healthToastRequested = false;
return healthState;
})().finally(() => {
healthRefreshPromise = null;
});
return healthRefreshPromise;
}
function startHealthPolling() {
if (healthPollTimer !== null) return;
healthPollTimer = window.setInterval(() => {
if (document.visibilityState === "visible"
&& document.body.dataset.module === "self-extension") refreshHealth();
}, HEALTH_POLL_INTERVAL_MS);
}
function stopHealthPolling() {
if (healthPollTimer !== null) window.clearInterval(healthPollTimer);
healthPollTimer = null;
}
async function resetSession() {
@@ -1208,6 +1512,19 @@
refreshHealth();
}
});
window.addEventListener("cognitive-agent:result", (event) => {
renderAuthoritativeVehicleState(event.detail);
});
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible"
&& document.body.dataset.module === "self-extension") refreshHealth();
});
window.addEventListener("pagehide", stopHealthPolling);
window.addEventListener("pageshow", () => {
startHealthPolling();
if (document.visibilityState === "visible"
&& document.body.dataset.module === "self-extension") refreshHealth();
});
}
function init() {
@@ -1216,6 +1533,7 @@
renderAll();
setMode(activeMode);
if (document.body.dataset.module === "self-extension") refreshHealth();
startHealthPolling();
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
}
+4 -4
View File
@@ -1,18 +1,18 @@
const CACHE_VERSION = "cognitive-agent-pad-v40";
const CACHE_VERSION = "cognitive-agent-pad-v52";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [
"/",
"/index.html",
"/styles.css?v=android-pad-4",
"/styles.css?v=android-pad-5",
"/network.js?v=android-pad-1",
"/app.js?v=seat-hil-44",
"/app.js?v=seat-hil-51",
"/gesture-control.js?v=android-pad-6",
"/pwa.js?v=android-pad-1",
"/console-v2.css?v=desktop-refactor-68",
"/console-v2.js?v=desktop-refactor-28",
"/self-extension.css?v=extension-9",
"/self-extension.js?v=extension-6",
"/self-extension.js?v=extension-15",
"/self-calibration.css?v=calibration-4",
"/self-calibration.js?v=calibration-2",
"/self-repair.css?v=repair-17",
+6
View File
@@ -2415,6 +2415,12 @@ textarea:focus {
border-color: #f0d4a2;
}
.thermal-states > span.on[data-function="massage"] {
color: #7654a8;
background: #f0eafa;
border-color: #d9c9ee;
}
.thermal-states > span.on b {
color: inherit;
}