优化无线/有线CAN,以及控制指令解析
This commit is contained in:
+125
-42
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user