优化无线/有线CAN,以及控制指令解析

This commit is contained in:
minghanchen
2026-08-26 20:11:09 +08:00
parent 9ac8fdface
commit 4a0570706f
49 changed files with 8409 additions and 1289 deletions
+125 -42
View File
@@ -2478,8 +2478,8 @@ function twinDeviceForSeat(seat, actuatorState = state.seatTwin?.actuatorState |
...device,
position: {
...position,
// The seat-domain compatibility key is cushion_tilt, but seat-ota
// board axis 1 is physical height. Keep the 3D cushion neutral and
// The seat-domain compatibility key is cushion_tilt, but the native
// CAN0 firmware axis 1 is physical height. Keep the 3D cushion neutral and
// animate/display the firmware feedback as height.
cushion_tilt: { position_pct: 0, position_valid: true },
height: seatHeight,
@@ -4323,6 +4323,33 @@ function selectedVehicleTransport() {
|| null;
}
function isHardwareExecutionReady() {
return Boolean(
state.runtime?.hardware?.configured || selectedVehicleTransport()?.configured,
);
}
function hardwareExecutionIssue(
hardware = state.runtime?.hardware || {},
transport = selectedVehicleTransport() || {},
) {
const stateError = String(transport?.state_sync?.last_error || "").trim();
if (/VCI_OpenDevice failed/i.test(stateError)) {
return "ZLG 设备打开失败:请检查 USB 读写权限,或关闭占用适配器的其他程序";
}
if (stateError) return stateError;
if (transport.state_configured === true && transport.configured !== true) {
return "CAN 状态监听已配置,但真实控制授权尚未配置";
}
if (state.controlTransport === "wireless_mqtt") {
return "无线 MQTT 控制后端尚未配置";
}
if (hardware.enabled === true && hardware.adapter_configured !== true) {
return "副驾 CH0 CAN 适配器尚未配置";
}
return "ZLG 有线 CAN 后端尚未通过执行配置校验";
}
function isDriverOnlyCh1Runtime() {
const transport = selectedVehicleTransport();
return transport?.configured === true
@@ -4343,7 +4370,9 @@ function renderControlTransport() {
// shown as offline, but it must never disable or silently change the
// operator's wired/wireless selection.
button.disabled = false;
button.dataset.connectionState = runtime?.state_sync?.state === "online" ? "online" : "offline";
button.dataset.connectionState = ["online", "ready"].includes(runtime?.state_sync?.state)
? "online"
: "offline";
button.title = runtime?.state_configured === true
? runtime?.state_sync?.last_error || ""
: `${id === "wireless_mqtt" ? "无线 MQTT" : "有线 CAN"} 当前未配置(仍可手动选择)`;
@@ -4362,8 +4391,16 @@ function renderControlTransport() {
const connection = selected?.state_sync?.state === "online" ? "已连接" : "未连接";
note.textContent = `MQTT ${connection} · 状态 ${selected?.condition_topic || "n7/n7-headunit/can/state"} · 控制 ${selected?.control_topic || "n7/n7-headunit/can/command"}`;
} else {
const connection = selected?.state_sync?.state === "online" ? "已连接" : "未连接";
note.textContent = `ZLG 有线 ${connection} · CH${selected?.channel ?? 1} · CAN FD 500k/2M`;
const connection = selected?.state_sync?.state === "online"
? "已连接"
: selected?.state_sync?.state === "ready"
? "已配置"
: "未连接";
const dualChannel = selected?.target_policy
=== "dual_channel_wired_can0_passenger_can1_vehicle";
note.textContent = dualChannel
? `ZLG 有线 ${connection} · CAN0 副驾经典 CAN 500k · CAN1 车辆 CAN FD 500k/2M`
: `ZLG 有线 ${connection} · CH${selected?.channel ?? 1} · CAN FD 500k/2M`;
}
}
@@ -4445,11 +4482,7 @@ function updateSeatFeedbackModeLabel() {
}
function selectExecutionMode(mode, { remember = true } = {}) {
const selectedTransport = selectedVehicleTransport();
const hardwareExecutionReady = Boolean(
state.runtime?.hardware?.configured || selectedTransport?.configured,
);
if (mode === "hardware" && !hardwareExecutionReady) return false;
const hardwareExecutionReady = isHardwareExecutionReady();
if (mode === "simulated_hardware" && !state.runtime?.simulated_hardware?.enabled) return false;
if (remember) state.preferredExecutionMode = mode;
state.executionMode = mode;
@@ -4476,6 +4509,9 @@ function selectExecutionMode(mode, { remember = true } = {}) {
? "真实执行"
: simulatedHardware ? "运行故障仿真" : "生成并执行";
}
if (remember && mode === "hardware" && !hardwareExecutionReady) {
showToast(`已切换到真实台架;当前不可执行:${hardwareExecutionIssue()}`);
}
updateSeatFeedbackModeLabel();
refreshIcons();
return true;
@@ -5132,7 +5168,9 @@ function modeEditorReadableCommands(flow) {
Object.values(plans).forEach((plan) => {
modeEditorPlanCommands(plan).forEach((item) => {
const debugCommand = String(item?.signals?.debug_command || item?.message || "CAN frame");
const channel = modeEditorCanId(item?.signals?.frame_id) === "0x6A0"
const channel = new Set(["0x29E", "0x300", "0x3CE"]).has(
modeEditorCanId(item?.signals?.frame_id),
)
? "CAN0"
: "CAN1";
const readable = { channel, category: modeEditorCommandCategory(debugCommand), command: debugCommand };
@@ -5798,7 +5836,13 @@ async function loadRuntime() {
syncReasoningControls();
$("#matrixCount").textContent = `${state.runtime.matrix.messages} / ${state.runtime.matrix.signals}`;
const hardwareButton = $("#hardwareMode");
hardwareButton.disabled = !hardwareExecutionReady;
// “真实台架”是操作员手动选择的执行目标。硬件暂不可用时仍允许
// 选择该目标并展示诊断信息;真正的控制提交继续由后端安全门拦截。
hardwareButton.disabled = false;
hardwareButton.dataset.executionState = hardwareExecutionReady ? "available" : "blocked";
hardwareButton.title = hardwareExecutionReady
? "真实台架已通过后端执行配置校验"
: `可手动选择真实台架;当前不可执行:${hardwareExecutionIssue(hardware, vehicleCan)}`;
const simulation = state.runtime.simulated_hardware || {};
$("#simulationMode").disabled = !simulation.enabled;
$("#simulationMode").title = simulation.enabled
@@ -5822,7 +5866,7 @@ async function loadRuntime() {
? `${mqttDualChannel ? "MQTT CAN0 副驾座椅 · MQTT CAN1 其余车辆" : `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s`} · ${vehicleRouteText}${mqttDualChannel ? " · 远端自动烧录禁用" : ` · ${profileText} · ${flashText}`}${accessText}${concurrencyText}`
: seatHardwareReady
? `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s · ${profileText} · ${flashText} · 车端 CAN 未启用,其他领域保持虚拟执行${accessText}${concurrencyText}`
: `真实执行已锁定:${profileText};当前${mqttSelected ? "无线 MQTT" : "有线 CAN"}控制未通过后端审批配置`;
: `可手动选择真实台架;当前不可执行:${hardwareExecutionIssue(hardware, vehicleCan)}`;
$("#hardwareAliasNote").textContent = ch1DriverOnly && vehicleHardwareReady && !seatHardwareReady
? `${mqttSelected ? "MQTT" : "CH1"} → 车身 CAN / 主驾座椅 · CH0 与副驾 T2G 路径禁止`
: linkedHardwareReady
@@ -5839,9 +5883,7 @@ async function loadRuntime() {
: "已阻断";
$("#hardwareCoverage").className = hardwareExecutionReady ? "available" : "blocked";
const preferredMode = state.preferredExecutionMode || DEFAULT_EXECUTION_MODE;
const modeAvailable = preferredMode === "hardware"
? hardwareExecutionReady
: preferredMode === "simulated_hardware"
const modeAvailable = preferredMode === "simulated_hardware"
? simulation.enabled
: true;
selectExecutionMode(modeAvailable ? preferredMode : "dry_run", { remember: false });
@@ -6725,6 +6767,7 @@ const CONFIG_CAPABILITY_LABELS = {
"window.set_motion": "控制车窗动作",
"window.set_position": "设置车窗开度",
"outlet.set_position_percent": "设置电动风口位置",
"outlet.set_switch_state": "开关单个电动风口",
"outlet.set_calibrated_position": "设置电动风口标定位置",
"interior_light.set_indoor_power": "设置室内灯",
"interior_light.set_wall_power": "设置壁灯",
@@ -6991,6 +7034,38 @@ function renderCode(result) {
}
}
function operationVerificationStatus(item = {}) {
const explicit = String(item.telemetry?.verification_status || "");
if (["verified_success", "submitted_unverified", "failed_or_blocked"].includes(explicit)) {
return explicit;
}
if (item.status !== "succeeded") return "failed_or_blocked";
if (item.telemetry?.physical_effect_confirmed === true || item.telemetry?.feedback_frame) {
return "verified_success";
}
if (item.telemetry?.command_frame
|| ["submitted", "queued", "published"].includes(item.telemetry?.delivery_status)) {
return "submitted_unverified";
}
return "verified_success";
}
function executionVerificationStatus(result = {}) {
const explicit = String(
result.execution_outcome
|| result.execution_report?.spec?.verification_status
|| "",
);
if (["verified_success", "submitted_unverified", "failed_or_blocked"].includes(explicit)) {
return explicit;
}
const operations = result.execution_report?.spec?.results || [];
const statuses = operations.map(operationVerificationStatus);
if (statuses.includes("failed_or_blocked")) return "failed_or_blocked";
if (statuses.includes("submitted_unverified")) return "submitted_unverified";
return "verified_success";
}
function renderExecution(result) {
const summary = $("#executionSummary");
const list = $("#resultList");
@@ -7011,9 +7086,11 @@ function renderExecution(result) {
refreshIcons();
return;
}
const verificationStatus = executionVerificationStatus(result);
const submittedUnverified = verificationStatus === "submitted_unverified";
const reportSucceeded = report.spec.status === "succeeded";
const partiallySucceeded = result.status === "partially_succeeded";
summary.className = `execution-summary ${reportSucceeded ? "succeeded" : partiallySucceeded ? "partial" : "failed"}`;
summary.className = `execution-summary ${submittedUnverified || partiallySucceeded ? "partial" : reportSucceeded ? "succeeded" : "failed"}`;
const presetSave = Boolean(result.preset_config);
const modeSave = Boolean(result.mode_profile);
const simulatedHil = report.metadata?.labels?.seat_feedback === "simulated_hardware";
@@ -7024,14 +7101,12 @@ function renderExecution(result) {
const operationResults = report.spec.results.filter((item) => item.ref !== "seat-controller-firmware");
const succeededOperations = operationResults.filter((item) => item.status === "succeeded");
const failedOperations = operationResults.filter((item) => item.status === "failed");
const mqttSubmittedOperations = operationResults.filter(
(item) => ["queued", "published"].includes(item.telemetry?.delivery_status),
const submittedOperations = operationResults.filter(
(item) => operationVerificationStatus(item) === "submitted_unverified",
);
const physicallyConfirmedOperations = operationResults.filter(
(item) => item.telemetry?.physical_effect_confirmed === true,
);
const mqttPublishedUnconfirmed = result.control_transport === "wireless_mqtt"
&& mqttSubmittedOperations.length > physicallyConfirmedOperations.length;
const succeededDomains = [...new Set(succeededOperations.map((item) => item.domain))];
const failedDomains = [...new Set(failedOperations.map((item) => item.domain))];
let executionDetail = modeSave
@@ -7041,8 +7116,10 @@ function renderExecution(result) {
: simulatedHil
? `${executedDomains.join(" / ")} 已完成隔离 HIL 仿真,未访问物理 CAN`
: physical
? result.control_transport === "wireless_mqtt"
? `${executedDomains.join(" / ")} 已向 MQTT 提交 ${mqttSubmittedOperations.length || succeededOperations.length} 个白名单控制步骤${mqttPublishedUnconfirmed ? ";车辆效果尚未确认,以状态话题回读为准" : `,并确认 ${physicallyConfirmedOperations.length || succeededOperations.length} 个车辆状态`}`
? submittedUnverified
? `${executedDomains.join(" / ")} 已通过${result.control_transport === "wireless_mqtt" ? " MQTT" : "有线 CAN"}提交 ${submittedOperations.length || succeededOperations.length} 个白名单控制步骤;车辆效果尚未确认,以实时状态回读为准`
: result.control_transport === "wireless_mqtt"
? `${executedDomains.join(" / ")} 已向 MQTT 提交 ${succeededOperations.length} 个白名单控制步骤,并确认 ${physicallyConfirmedOperations.length || succeededOperations.length} 个车辆状态`
: selectedVehicleTransport()?.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} 个步骤`
@@ -7056,7 +7133,7 @@ function renderExecution(result) {
if (result.deferred_requests?.length) {
executionDetail += ";未接入域请求已保留为 deferred request";
}
if (!reportSucceeded && !partiallySucceeded) {
if (verificationStatus === "failed_or_blocked" && !partiallySucceeded) {
const failure = report.spec.results.find((item) => item.status === "failed");
const failureTelemetry = failure?.telemetry || {};
executionDetail = simulatedHil
@@ -7082,26 +7159,31 @@ function renderExecution(result) {
: presetSave ? "姿态预设已保存"
: simulatedHil ? "模拟台架执行成功"
: physical
? mqttPublishedUnconfirmed ? "MQTT 指令已提交" : "真实台架执行成功"
? submittedUnverified
? `${result.control_transport === "wireless_mqtt" ? "MQTT" : "CAN"} 指令已提交,等待状态确认`
: "真实台架执行并验证成功"
: "虚拟执行成功"
: partiallySucceeded ? (physical ? "真实台架部分执行成功" : "部分执行成功")
: simulatedHil ? "模拟故障已触发" : physical ? "真实台架执行失败" : "虚拟执行失败";
const executionIcon = reportSucceeded
const executionIcon = submittedUnverified
? "circle-alert"
: reportSucceeded
? ((presetSave || modeSave) ? "bookmark-check" : "circle-check-big")
: partiallySucceeded ? "circle-alert" : "circle-x";
summary.innerHTML = `<i data-lucide="${executionIcon}"></i><div><strong>${executionTitle}</strong><span>${executionDetail}</span></div>`;
$("#resultNavMeta").textContent = reportSucceeded
? mqttPublishedUnconfirmed
? `${mqttSubmittedOperations.length} 项已提交 · MQTT`
? submittedUnverified
? `${submittedOperations.length || succeededOperations.length} 项已提交 · 等待状态确认`
: `${report.spec.results.length} 项完成 · ${simulatedHil ? "SIM-HIL" : physical ? "HIL" : "DRY-RUN"}`
: partiallySucceeded ? `${succeededOperations.length} 项成功 / ${failedOperations.length} 项失败 · 查看明细`
: "执行失败 · 查看回读";
$("#resultPanelState").textContent = reportSucceeded ? "SUCCEEDED" : partiallySucceeded ? "PARTIAL" : "FAILED";
$("#resultPanelState").className = `workspace-panel-state ${reportSucceeded ? "succeeded" : partiallySucceeded ? "partial" : "failed"}`;
$("#resultPanelState").textContent = submittedUnverified ? "SUBMITTED" : reportSucceeded ? "VERIFIED" : partiallySucceeded ? "PARTIAL" : "FAILED";
$("#resultPanelState").className = `workspace-panel-state ${submittedUnverified || partiallySucceeded ? "partial" : reportSucceeded ? "succeeded" : "failed"}`;
report.spec.results.forEach((item) => {
const row = document.createElement("div");
row.className = "result-item";
row.innerHTML = `<i data-lucide="${item.status === "succeeded" ? "circle-check" : "circle-x"}"></i>`;
const itemVerification = operationVerificationStatus(item);
row.innerHTML = `<i data-lucide="${itemVerification === "verified_success" ? "circle-check" : itemVerification === "submitted_unverified" ? "circle-alert" : "circle-x"}"></i>`;
const text = document.createElement("div");
const title = document.createElement("strong");
title.textContent = item.ref;
@@ -7250,11 +7332,7 @@ function renderResult(result) {
state.result = result;
const reportMode = result.execution_report?.spec?.mode;
const feedbackSource = result.execution_report?.metadata?.labels?.seat_feedback;
const mqttPublishedUnconfirmed = result.control_transport === "wireless_mqtt"
&& (result.execution_report?.spec?.results || []).some(
(item) => ["queued", "published"].includes(item.telemetry?.delivery_status)
&& item.telemetry?.physical_effect_confirmed !== true,
);
const submittedUnverified = executionVerificationStatus(result) === "submitted_unverified";
if (feedbackSource === "simulated_hardware") {
state.seatFeedbackSource = "simulated_hardware";
if (result.actuator_state?.passenger) state.seatFeedbackSources.passenger = "simulated_hardware";
@@ -7289,10 +7367,10 @@ function renderResult(result) {
? "长期模式配置已保存"
: result.preset_config
? "座椅姿态预设已保存"
: result.scene_config
? `${domains.length} 域场景闭环完成`
: mqttPublishedUnconfirmed
? `${domainLabels[domains[0]] || "车舱"}控制指令已提交`
: submittedUnverified
? `${domains.length > 1 ? `${domains.length} 域场景` : domainLabels[domains[0]] || "车舱"}控制指令已提交`
: result.scene_config
? `${domains.length} 域场景闭环完成`
: `${domainLabels[domains[0]] || "车舱"}单域闭环完成`
: "请求未执行";
const conversationLabel = result.conversation_id?.slice(-8) || "no-context";
@@ -7302,8 +7380,8 @@ function renderResult(result) {
$("#requestMeta").textContent += ` / reasoning:${result.reasoning_effort_used}`;
}
setRunStatus(
partiallySucceeded ? "partial" : executionSucceeded ? "succeeded" : "failed",
result.status.toUpperCase(),
partiallySucceeded || submittedUnverified ? "partial" : executionSucceeded ? "succeeded" : "failed",
submittedUnverified ? "SUBMITTED_UNVERIFIED" : result.status.toUpperCase(),
);
renderDecision(result);
renderTrace(result.trace_events || []);
@@ -7353,6 +7431,8 @@ async function postControlPhase(path, body, executionMode, onEvent = null) {
phaseEvent.message || phaseEvent.error || "流式控制请求失败",
);
requestError.code = phaseEvent.error;
requestError.outcome = phaseEvent.outcome;
requestError.detail = phaseEvent.detail;
requestError.retryAfter = Number(phaseEvent.retry_after_seconds || 0);
throw requestError;
}
@@ -7572,6 +7652,7 @@ async function submitRequest(event) {
setModeExecutionState(submittedModeProfileId, completed ? "succeeded" : "failed");
}
const simulated = payload.execution_report?.metadata?.labels?.seat_feedback === "simulated_hardware";
const submittedUnverified = executionVerificationStatus(payload) === "submitted_unverified";
showToast(supplementWarning || (completed
? (payload.mode_profile
? `${payload.mode_profile.name}已创建并添加到功能模式`
@@ -7579,6 +7660,8 @@ async function submitRequest(event) {
? "已执行可用设备;部分设备未完成,详见执行结果"
: simulated
? "模拟台架执行完成"
: submittedUnverified
? "CAN 控制指令已提交;等待车辆实时状态确认"
: payload.execution_report?.spec?.mode === "hil"
? "真实台架执行与反馈确认已完成"
: "可执行动作的 dry-run 已完成")
+117 -25
View File
@@ -22,6 +22,31 @@
let reasoningPickerControl = null;
let validatedCommandsVisible = false;
let validatedCommandContext = { requestId: "", utterance: "" };
function resetValidatedCommandContext() {
validatedCommandsVisible = false;
validatedCommandContext = { requestId: "", utterance: "" };
}
function rememberValidatedCommandContext(result = {}) {
validatedCommandContext = {
requestId: String(result.request_id || ""),
utterance: String(result.utterance || "").trim(),
};
}
function validatedCommandsBelongToTask(task = {}) {
if (!validatedCommandsVisible) return false;
const result = task.result && typeof task.result === "object" ? task.result : {};
const requestId = String(result.request_id || "");
const utterance = String(task.utterance || result.utterance || "").trim();
if (validatedCommandContext.requestId && requestId
&& validatedCommandContext.requestId !== requestId) return false;
if (validatedCommandContext.utterance && utterance
&& validatedCommandContext.utterance !== utterance) return false;
return true;
}
function make(tag, className, html) {
const node = document.createElement(tag);
@@ -71,10 +96,13 @@
...(parseSource ? { parseSource } : {}),
});
const hexByte = (value) => Number(value).toString(16).padStart(2, "0").toUpperCase();
const otaCan0Frame = (opcode, selector, argument) => {
const numeric = Number(argument) >>> 0;
const argumentBytes = [0, 8, 16, 24].map((shift) => hexByte((numeric >>> shift) & 0xFF));
return `0x6A0 · C7 <SEQ> ${hexByte(opcode)} ${hexByte(selector)} ${argumentBytes.join(" ")}`;
const littleEndian16 = (value) => {
const numeric = Number(value) & 0xFFFF;
return `${hexByte(numeric & 0xFF)} ${hexByte((numeric >>> 8) & 0xFF)}`;
};
const nativeCan0Frame = (opcode, motor, direction, paramA, paramB) => {
const control = (Number(motor) & 0x03) | ((Number(direction) & 0x03) << 2) | 0x10;
return `0x300 · ${hexByte(opcode)} ${hexByte(control)} <SEQ_LO> <SEQ_HI> ${littleEndian16(paramA)} ${littleEndian16(paramB)}`;
};
const isPassengerSeat = new Set(["passenger", "front-right"]).has(target);
@@ -88,7 +116,6 @@
const axis = passengerAxes[capability];
const position = boundedInteger(parameters.position_pct, 0, 100);
if (axis && position !== null) {
const targetPermille = position * 10;
const motion = operation.motion || {};
const delta = boundedInteger(motion.delta_position_pct, 1, 100);
const relativeDirections = {
@@ -104,7 +131,9 @@
? `seat passenger ${axis[1]} relative ${relativeDirection} ${delta}`
: `seat passenger ${axis[1]} position ${position}`,
"CAN0",
otaCan0Frame(0x64, axis[2], targetPermille),
position === 0
? nativeCan0Frame(0x0B, axis[2], 0, 0x4B52, 120)
: nativeCan0Frame(0x01, axis[2], 0, position * 100, 30000),
);
}
if (new Set(["seat.set_ventilation", "seat.set_heating"]).has(capability)) {
@@ -124,14 +153,12 @@
if (capability === "seat.start_dynamic_mode") {
const mode = boundedInteger(parameters.mode_id, 1, 2);
if (mode !== null) {
const dynamic = mode === 1
? { name: "rocking-horse", selector: 2 }
: { name: "cradle", selector: 1 };
const dynamic = mode === 1 ? "rocking-horse" : "cradle";
return command(
"副驾座椅动态模式",
`seat passenger dynamic ${dynamic.name}`,
`seat passenger dynamic ${dynamic}`,
"CAN0",
otaCan0Frame(0x66, dynamic.selector, 300),
nativeCan0Frame(0x0E, 0, 0, mode, 0),
);
}
}
@@ -140,7 +167,7 @@
"副驾座椅动态模式",
"seat passenger dynamic stop",
"CAN0",
otaCan0Frame(0x63, 0xFF, 0),
nativeCan0Frame(0x0F, 0, 0, 0, 0),
);
}
}
@@ -198,6 +225,13 @@
const mode = String(parameters.mode ?? parameters.cycle_mode ?? "");
if (new Set(["internal", "external"]).has(mode)) return command("空调循环", `ac cycle ${mode}`);
}
if (capability === "outlet.set_switch_state") {
const state = String(parameters.state ?? "");
if (new Set(["driver-left", "driver-right", "passenger-left", "passenger-right"]).has(target)
&& new Set(["open", "close"]).has(state)) {
return command("单风口开关", `outlet ${target} ${state}`);
}
}
if (capability === "outlet.set_position_percent") {
const horizontal = boundedInteger(parameters.horizontal_pct, 0, 100);
const vertical = boundedInteger(parameters.vertical_pct, 0, 100);
@@ -330,7 +364,7 @@
${escapeHtml(item.category)}
</span>
<code>${escapeHtml(item.command)}</code>
${item.frame ? `<small class="v2-can-command-frame">seat-ota · ${escapeHtml(item.frame)}</small>` : ""}
${item.frame ? `<small class="v2-can-command-frame">固件原生 CAN0 · ${escapeHtml(item.frame)}</small>` : ""}
</li>`).join("")}</ol>`;
} else {
list.innerHTML = `<p>${escapeHtml(message || "本次请求未生成已验证的 CAN 控制指令")}</p>`;
@@ -359,6 +393,10 @@
<code>${escapeHtml(item.command)}</code>
</li>`).join("")}</ol>`;
validatedCommandsVisible = true;
validatedCommandContext = {
requestId: "",
utterance: items.map((item) => item.command).join(";"),
};
requestAnimationFrame(syncSidebarLogBaseline);
}
@@ -1184,6 +1222,12 @@
if ($("#v2TaskKind")) $("#v2TaskKind").textContent = kind;
if ($("#v2TaskCommand")) $("#v2TaskCommand").textContent = utterance || "等待车机端或网页端选择预设模式、输入一句话指令";
const normalizedStatus = String(task.status || "idle").toLowerCase();
const verificationStatus = String(
result?.execution_outcome
|| result?.execution_report?.spec?.verification_status
|| "",
);
const submittedUnverified = verificationStatus === "submitted_unverified";
const statusMap = {
idle: ["等待任务", "idle"],
running: ["构建 / 执行中", "running"],
@@ -1192,7 +1236,9 @@
needs_confirmation: ["等待确认", "partial"],
failed: ["执行失败", "failed"],
};
const [label, state] = statusMap[normalizedStatus] || statusMap.idle;
const [label, state] = submittedUnverified && normalizedStatus === "succeeded"
? ["指令已提交 · 等待状态确认", "partial"]
: statusMap[normalizedStatus] || statusMap.idle;
const badge = $("#v2TaskState");
if (badge) {
badge.textContent = label;
@@ -1207,9 +1253,23 @@
});
renderTaskTimeline(task);
if (normalizedStatus === "running") {
renderParsedCanCommands(null, { message: "正在解析自然语言并生成 CAN 控制指令…" });
// The streaming endpoint can publish fully validated commands before the
// slower display-sync poll observes the terminal execution state. A stale
// `running` snapshot for the same task must not erase those commands.
if (!validatedCommandsBelongToTask(task)) {
resetValidatedCommandContext();
renderParsedCanCommands(null, { message: "正在解析自然语言并生成 CAN 控制指令…" });
}
} else if (result) {
renderParsedCanCommands(result);
const commands = parsedCanCommands(result);
if (commands.length) {
validatedCommandsVisible = true;
rememberValidatedCommandContext(result);
renderParsedCanCommands(result);
} else if (!validatedCommandsBelongToTask(task)) {
resetValidatedCommandContext();
renderParsedCanCommands(result);
}
}
}
@@ -1245,12 +1305,14 @@
setTaskPresentation(task);
const sourceLabel = task.source === "web_console" ? "网页端" : "车机端";
if (task.utterance) {
const submittedUnverified = task.result?.execution_outcome === "submitted_unverified"
|| task.result?.execution_report?.spec?.verification_status === "submitted_unverified";
appendSyncLog({
key: `task:${revision}:${task.status}`,
category: "task",
title: `${sourceLabel}${task.status === "running" ? "下发编排任务" : "任务状态已同步"}`,
detail: `${task.utterance} · ${task.status || "idle"}`,
status: ["succeeded", "partially_succeeded"].includes(task.status) ? "success" : task.status === "failed" ? "error" : "info",
detail: `${task.utterance} · ${submittedUnverified ? "指令已提交,等待车辆状态确认" : task.status || "idle"}`,
status: submittedUnverified ? "info" : ["succeeded", "partially_succeeded"].includes(task.status) ? "success" : task.status === "failed" ? "error" : "info",
});
}
const result = task.result;
@@ -1487,26 +1549,44 @@
});
$("#v2LogFilter")?.addEventListener("change", renderSyncLogs);
$("#requestForm")?.addEventListener("submit", () => {
validatedCommandsVisible = false;
resetValidatedCommandContext();
renderParsedCanCommands(null, { message: "正在解析自然语言并生成 CAN 控制指令…" });
});
window.addEventListener("cognitive-agent:validated-can-commands", (event) => {
const result = event.detail || {};
validatedCommandsVisible = parsedCanCommands(result).length > 0;
if (validatedCommandsVisible) rememberValidatedCommandContext(result);
else resetValidatedCommandContext();
renderParsedCanCommands(result, {
message: "已完成白名单校验,正在下发控制指令…",
});
});
window.addEventListener("cognitive-agent:result", (event) => {
const result = event.detail || {};
validatedCommandsVisible = parsedCanCommands(result).length > 0;
renderParsedCanCommands(result);
const submittedUnverified = result.execution_outcome === "submitted_unverified"
|| result.execution_report?.spec?.verification_status === "submitted_unverified";
const commands = parsedCanCommands(result);
if (commands.length) {
validatedCommandsVisible = true;
rememberValidatedCommandContext(result);
renderParsedCanCommands(result);
} else if (!validatedCommandsBelongToTask({
utterance: result.utterance,
result,
})) {
resetValidatedCommandContext();
renderParsedCanCommands(result);
}
appendSyncLog({
key: result.request_id ? `result:${result.request_id}` : "",
category: "task",
title: ["succeeded", "partially_succeeded"].includes(result.status) ? "编排任务执行完成" : "编排任务返回结果",
detail: `${result.summary || result.message || "执行反馈已同步"} · ${result.status || "unknown"}`,
status: ["succeeded", "partially_succeeded"].includes(result.status) ? "success" : result.status === "failed" ? "error" : "info",
title: submittedUnverified
? "CAN 指令已提交,等待车辆状态确认"
: ["succeeded", "partially_succeeded"].includes(result.status)
? "编排任务执行完成"
: "编排任务返回结果",
detail: `${result.summary || result.message || "执行反馈已同步"} · ${submittedUnverified ? "submitted_unverified" : result.status || "unknown"}`,
status: submittedUnverified ? "info" : ["succeeded", "partially_succeeded"].includes(result.status) ? "success" : result.status === "failed" ? "error" : "info",
});
});
window.addEventListener("cognitive-agent:request-complete", (event) => {
@@ -1514,7 +1594,19 @@
renderParsedCanCommands(null, { message: "本次请求未生成 CAN 控制指令" });
}
if (event.detail?.error) {
appendSyncLog({ category: "task", title: "编排任务执行失败", detail: event.detail.error.message || "请求失败", status: "error" });
const error = event.detail.error;
const outcomeLabels = {
failed_or_blocked: "失败或已阻止",
submitted_unverified: "已提交但未验证",
verified_success: "反馈验证成功",
};
const outcome = outcomeLabels[error.outcome];
appendSyncLog({
category: "task",
title: "编排任务执行失败",
detail: `${error.detail || error.message || "请求失败"}${outcome ? ` · ${outcome}` : ""}`,
status: "error",
});
}
});
window.addEventListener("cognitive-agent:connection", (event) => {
+3 -3
View File
@@ -17,10 +17,10 @@
<script src="/vendor/three/0.147.0/fflate.min.js" defer></script>
<script src="/vendor/three/0.147.0/FBXLoader.js" defer></script>
<script src="/network.js?v=android-pad-2" defer></script>
<script src="/app.js?v=dual-seat-103" defer></script>
<script src="/app.js?v=dual-seat-107" defer></script>
<script type="module" src="/gesture-control.js?v=android-pad-9"></script>
<script src="/pwa.js?v=android-pad-3" defer></script>
<script src="/console-v2.js?v=sync-layout-43" defer></script>
<script src="/console-v2.js?v=task-outcome-47" defer></script>
<script src="/self-extension.js?v=sync-layout-26" defer></script>
</head>
<body class="console-v2">
@@ -111,7 +111,7 @@
<button type="button" class="mode-option" data-execution-mode="dry_run">
<i data-lucide="flask-conical"></i>虚拟 CAN
</button>
<button type="button" class="mode-option active" id="hardwareMode" data-execution-mode="hardware" disabled>
<button type="button" class="mode-option active" id="hardwareMode" data-execution-mode="hardware">
<i data-lucide="radio-tower"></i>真实台架
</button>
<button type="button" class="mode-option" id="simulationMode" data-execution-mode="simulated_hardware" disabled>
+3 -3
View File
@@ -1,4 +1,4 @@
const CACHE_VERSION = "cognitive-agent-pad-v204";
const CACHE_VERSION = "cognitive-agent-pad-v210";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [
@@ -6,11 +6,11 @@ const APP_SHELL = [
"/index.html",
"/styles.css?v=dual-seat-16",
"/network.js?v=android-pad-2",
"/app.js?v=dual-seat-103",
"/app.js?v=dual-seat-107",
"/gesture-control.js?v=android-pad-9",
"/pwa.js?v=android-pad-3",
"/console-v2.css?v=sync-layout-41",
"/console-v2.js?v=sync-layout-43",
"/console-v2.js?v=task-outcome-47",
"/self-extension.css?v=sync-layout-26",
"/self-extension.js?v=sync-layout-26",
"/assets/pets/self-extension-cat.png?v=3",