优化后端和前端逻辑
This commit is contained in:
+222
-179
@@ -70,10 +70,10 @@ const state = {
|
||||
physicalStatePoller: null,
|
||||
physicalStateSyncPromise: null,
|
||||
passengerStateSyncPromise: null,
|
||||
wiredCanProbe: { passenger: false, vehicle: false },
|
||||
feedbackSignals: { passenger: false, vehicle: false },
|
||||
pendingModeProfileId: null,
|
||||
modeRunTimer: null,
|
||||
modeDraftCommand: "",
|
||||
lastSuccessfulUtterance: "",
|
||||
modeEditor: null,
|
||||
directCanCommandPending: false,
|
||||
};
|
||||
@@ -133,6 +133,13 @@ function renderCabinLiveStatus(networkStatus = state.connectionStatus) {
|
||||
setSummarySystem("链路未配置", "waiting");
|
||||
return;
|
||||
}
|
||||
if (!transportConnectionOnline(state.controlTransport, selected)) {
|
||||
indicator.dataset.state = "offline";
|
||||
label.textContent = `${transportLabel} · 信号链路断开`;
|
||||
indicator.title = selected?.state_sync?.last_error || "尚未检测到车辆通信设备";
|
||||
setSummarySystem("链路断开", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = state.vehicleStateSnapshot;
|
||||
const observedIds = Array.isArray(snapshot?.observedFrameIds)
|
||||
@@ -4323,6 +4330,54 @@ function selectedVehicleTransport() {
|
||||
|| null;
|
||||
}
|
||||
|
||||
function wiredCanConnectionOnline(runtime = state.runtime?.vehicle_can) {
|
||||
return runtime?.state_sync?.state === "online"
|
||||
|| state.wiredCanProbe.passenger === true
|
||||
|| state.wiredCanProbe.vehicle === true;
|
||||
}
|
||||
|
||||
function transportConnectionOnline(id, runtime) {
|
||||
return id === "wired_can"
|
||||
? wiredCanConnectionOnline(runtime)
|
||||
: runtime?.state_sync?.state === "online";
|
||||
}
|
||||
|
||||
function setFeedbackSignal(scope, connected, requestedTransport = state.controlTransport) {
|
||||
if (requestedTransport !== state.controlTransport) return;
|
||||
const next = connected === true;
|
||||
const datasetKey = `${scope}FeedbackState`;
|
||||
const datasetValue = next ? "online" : "offline";
|
||||
if (state.feedbackSignals[scope] === next
|
||||
&& document.body?.dataset[datasetKey] === datasetValue) return;
|
||||
state.feedbackSignals[scope] = next;
|
||||
if (document.body) {
|
||||
document.body.dataset[datasetKey] = datasetValue;
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-feedback-state", {
|
||||
detail: { scope, connected: next, control_transport: requestedTransport },
|
||||
}));
|
||||
}
|
||||
|
||||
function updateWiredCanProbe(scope, connected, requestedTransport) {
|
||||
if (requestedTransport !== "wired_can" || requestedTransport !== state.controlTransport) return;
|
||||
const next = connected === true;
|
||||
if (state.wiredCanProbe[scope] === next) return;
|
||||
state.wiredCanProbe[scope] = next;
|
||||
renderControlTransport();
|
||||
}
|
||||
|
||||
function resetTransportConnectionEvidence() {
|
||||
state.wiredCanProbe.passenger = false;
|
||||
state.wiredCanProbe.vehicle = false;
|
||||
["passenger", "vehicle"].forEach((scope) => {
|
||||
state.feedbackSignals[scope] = false;
|
||||
if (document.body) document.body.dataset[`${scope}FeedbackState`] = "offline";
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-feedback-state", {
|
||||
detail: { scope: "all", connected: false, control_transport: state.controlTransport },
|
||||
}));
|
||||
}
|
||||
|
||||
function isHardwareExecutionReady() {
|
||||
return Boolean(
|
||||
state.runtime?.hardware?.configured || selectedVehicleTransport()?.configured,
|
||||
@@ -4370,9 +4425,14 @@ 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 = ["online", "ready"].includes(runtime?.state_sync?.state)
|
||||
const connectionState = transportConnectionOnline(id, runtime)
|
||||
? "online"
|
||||
: "offline";
|
||||
button.dataset.connectionState = connectionState;
|
||||
button.setAttribute(
|
||||
"aria-label",
|
||||
`${id === "wireless_mqtt" ? "无线 CAN" : "有线 CAN"},信号链路${connectionState === "online" ? "正常" : "断开"}`,
|
||||
);
|
||||
button.title = runtime?.state_configured === true
|
||||
? runtime?.state_sync?.last_error || ""
|
||||
: `${id === "wireless_mqtt" ? "无线 MQTT" : "有线 CAN"} 当前未配置(仍可手动选择)`;
|
||||
@@ -4388,14 +4448,10 @@ function renderControlTransport() {
|
||||
return;
|
||||
}
|
||||
if (state.controlTransport === "wireless_mqtt") {
|
||||
const connection = selected?.state_sync?.state === "online" ? "已连接" : "未连接";
|
||||
const connection = transportConnectionOnline("wireless_mqtt", selected) ? "已连接" : "未连接";
|
||||
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"
|
||||
? "已连接"
|
||||
: selected?.state_sync?.state === "ready"
|
||||
? "已配置"
|
||||
: "未连接";
|
||||
const connection = transportConnectionOnline("wired_can", selected) ? "已连接" : "未连接";
|
||||
const dualChannel = selected?.target_policy
|
||||
=== "dual_channel_wired_can0_passenger_can1_vehicle";
|
||||
note.textContent = dualChannel
|
||||
@@ -4414,6 +4470,7 @@ function selectControlTransport(transport) {
|
||||
sessionStorage.setItem("cognitive-agent.conversation-id", state.conversationId);
|
||||
state.seatFeedbackSources.passenger = "unavailable";
|
||||
state.seatFeedbackSources.driver = "unavailable";
|
||||
resetTransportConnectionEvidence();
|
||||
resetVehicleStatusDisplay();
|
||||
}
|
||||
state.vehicleStateSnapshot = null;
|
||||
@@ -4510,7 +4567,7 @@ function selectExecutionMode(mode, { remember = true } = {}) {
|
||||
: simulatedHardware ? "运行故障仿真" : "生成并执行";
|
||||
}
|
||||
if (remember && mode === "hardware" && !hardwareExecutionReady) {
|
||||
showToast(`已切换到真实台架;当前不可执行:${hardwareExecutionIssue()}`);
|
||||
showToast(`已切换到真实下发;当前不可执行:${hardwareExecutionIssue()}`);
|
||||
}
|
||||
updateSeatFeedbackModeLabel();
|
||||
refreshIcons();
|
||||
@@ -4811,6 +4868,43 @@ function createModeGalleryCard(mode) {
|
||||
return shell;
|
||||
}
|
||||
|
||||
function createModeGalleryAddCard() {
|
||||
const shell = document.createElement("article");
|
||||
shell.className = "mode-gallery-card-shell mode-gallery-add-card-shell";
|
||||
const button = document.createElement("button");
|
||||
button.className = "mode-gallery-card mode-gallery-add-card";
|
||||
button.type = "button";
|
||||
button.dataset.modeCreate = "true";
|
||||
|
||||
const iconBox = document.createElement("span");
|
||||
iconBox.className = "mode-gallery-card-icon";
|
||||
const icon = document.createElement("i");
|
||||
icon.dataset.lucide = "bookmark-plus";
|
||||
icon.dataset.icon = "inline-start";
|
||||
iconBox.append(icon);
|
||||
|
||||
const copy = document.createElement("span");
|
||||
copy.className = "mode-gallery-card-copy";
|
||||
const heading = document.createElement("span");
|
||||
heading.className = "mode-gallery-card-heading";
|
||||
const name = document.createElement("strong");
|
||||
name.textContent = "新增模式";
|
||||
const stateLabel = document.createElement("small");
|
||||
stateLabel.className = "mode-gallery-state";
|
||||
stateLabel.textContent = "点击创建";
|
||||
heading.append(name, stateLabel);
|
||||
const description = document.createElement("span");
|
||||
description.className = "mode-gallery-card-description";
|
||||
description.textContent = "用自然语言描述需要联动的车辆功能";
|
||||
const meta = document.createElement("span");
|
||||
meta.className = "mode-gallery-card-meta";
|
||||
meta.textContent = "解析 · 校验 · 生成控制指令";
|
||||
copy.append(heading, description, meta);
|
||||
button.append(iconBox, copy);
|
||||
shell.append(button);
|
||||
return shell;
|
||||
}
|
||||
|
||||
function populateModeGallery() {
|
||||
const container = $("#modeGalleryGroups");
|
||||
if (!container) return;
|
||||
@@ -4837,14 +4931,8 @@ function populateModeGallery() {
|
||||
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "mode-gallery-grid";
|
||||
if (group.modes.length) {
|
||||
group.modes.forEach((mode) => grid.append(createModeGalleryCard(mode)));
|
||||
} else {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "mode-gallery-empty";
|
||||
empty.innerHTML = '<i data-lucide="bookmark-plus"></i><span><strong>还没有自定义模式</strong><small>可在主界面根据当前指令创建</small></span>';
|
||||
grid.append(empty);
|
||||
}
|
||||
if (group.id === "personal") grid.append(createModeGalleryAddCard());
|
||||
group.modes.forEach((mode) => grid.append(createModeGalleryCard(mode)));
|
||||
section.append(grid);
|
||||
container.append(section);
|
||||
});
|
||||
@@ -4854,38 +4942,24 @@ function populateModeGallery() {
|
||||
refreshIcons();
|
||||
}
|
||||
|
||||
function updateModeRailNavigation() {
|
||||
const chips = $(".mode-chips");
|
||||
const previous = $("#modeRailPrev");
|
||||
const next = $("#modeRailNext");
|
||||
if (!chips || !previous || !next) return;
|
||||
const maxScroll = Math.max(0, chips.scrollWidth - chips.clientWidth);
|
||||
previous.disabled = maxScroll <= 2 || chips.scrollLeft <= 2;
|
||||
next.disabled = maxScroll <= 2 || chips.scrollLeft >= maxScroll - 2;
|
||||
function setModeRailExpanded(expanded) {
|
||||
const toggle = $("#modeRailToggle");
|
||||
const content = $("#modeRailContent");
|
||||
if (!toggle || !content) return;
|
||||
const open = expanded === true;
|
||||
toggle.setAttribute("aria-expanded", String(open));
|
||||
content.hidden = !open;
|
||||
}
|
||||
|
||||
function scrollModeRail(direction) {
|
||||
const chips = $(".mode-chips");
|
||||
if (!chips) return;
|
||||
const buttons = $$("button[data-mode-profile-id]", chips);
|
||||
const viewport = chips.getBoundingClientRect();
|
||||
const target = direction > 0
|
||||
? buttons.find((button) => button.getBoundingClientRect().right > viewport.right + 2)
|
||||
: [...buttons].reverse().find((button) => button.getBoundingClientRect().left < viewport.left - 2);
|
||||
if (target) {
|
||||
const targetLeft = direction > 0
|
||||
? target.offsetLeft - 6
|
||||
: target.offsetLeft + target.offsetWidth - chips.clientWidth + 6;
|
||||
chips.scrollTo({ left: Math.max(0, targetLeft), behavior: "smooth" });
|
||||
} else {
|
||||
chips.scrollBy({ left: direction * Math.max(220, chips.clientWidth * 0.72), behavior: "smooth" });
|
||||
}
|
||||
function setModeRailSelection(mode) {
|
||||
const selection = $("#modeRailSelection");
|
||||
if (selection && mode) selection.textContent = String(mode.name || mode.id || "常用模式");
|
||||
}
|
||||
|
||||
function openModeGallery() {
|
||||
const backdrop = $("#modeGalleryBackdrop");
|
||||
if (!backdrop) return;
|
||||
closeSaveModeComposer();
|
||||
setModeRailExpanded(false);
|
||||
backdrop.hidden = false;
|
||||
document.body.classList.add("mode-gallery-open");
|
||||
$("#modeGalleryToggle").setAttribute("aria-expanded", "true");
|
||||
@@ -5318,12 +5392,16 @@ function openModeEditor(modeProfileId) {
|
||||
: null;
|
||||
state.modeEditor = {
|
||||
modeId: mode.id,
|
||||
isNew: false,
|
||||
sourceRequestId: String(flow?.source_request_id || ""),
|
||||
executedInstruction: flow ? instruction : "",
|
||||
flow,
|
||||
restoreSelector: `[data-mode-edit="${CSS.escape(mode.id)}"]`,
|
||||
};
|
||||
$("#modeEditorTitle").textContent = "编辑功能模式";
|
||||
$("#modeEditorName").value = mode.name || "";
|
||||
$("#modeEditorInstruction").value = instruction;
|
||||
$("#modeEditorComplete").innerHTML = '<i data-lucide="save" data-icon="inline-start"></i><span>完成</span>';
|
||||
$("#modeEditorStatus").textContent = flow
|
||||
? "已载入永久保存的自然语言和控制指令流,可修改后重新生成。"
|
||||
: "修改自然语言后需要重新生成指令,才能点击完成永久保存。";
|
||||
@@ -5338,18 +5416,44 @@ function openModeEditor(modeProfileId) {
|
||||
refreshIcons();
|
||||
}
|
||||
|
||||
function openModeEditorForCreate({ restoreSelector = "#saveModeToggle" } = {}) {
|
||||
const backdrop = $("#modeEditorBackdrop");
|
||||
if (!backdrop) return;
|
||||
const instruction = $("#utterance")?.value.trim() || "";
|
||||
state.modeEditor = {
|
||||
modeId: null,
|
||||
isNew: true,
|
||||
sourceRequestId: "",
|
||||
executedInstruction: "",
|
||||
flow: null,
|
||||
restoreSelector,
|
||||
};
|
||||
$("#modeEditorTitle").textContent = "新增功能模式";
|
||||
$("#modeEditorName").value = "";
|
||||
$("#modeEditorInstruction").value = instruction;
|
||||
$("#modeEditorStatus").textContent = "输入模式名称和自然语言,生成控制指令后即可添加模式卡片。";
|
||||
$("#modeEditorComplete").innerHTML = '<i data-lucide="bookmark-plus" data-icon="inline-start"></i><span>添加模式</span>';
|
||||
setModeEditorFlowExpanded(false);
|
||||
backdrop.hidden = false;
|
||||
document.body.classList.add("mode-editor-open");
|
||||
renderModeEditorFlow();
|
||||
window.requestAnimationFrame(() => {
|
||||
backdrop.classList.add("open");
|
||||
(instruction ? $("#modeEditorName") : $("#modeEditorInstruction")).focus();
|
||||
});
|
||||
refreshIcons();
|
||||
}
|
||||
|
||||
function closeModeEditor({ restoreFocus = true } = {}) {
|
||||
const backdrop = $("#modeEditorBackdrop");
|
||||
const modeId = state.modeEditor?.modeId;
|
||||
const restoreSelector = state.modeEditor?.restoreSelector;
|
||||
if (!backdrop || backdrop.hidden) return;
|
||||
backdrop.classList.remove("open");
|
||||
backdrop.hidden = true;
|
||||
setModeEditorFlowExpanded(false);
|
||||
document.body.classList.remove("mode-editor-open");
|
||||
state.modeEditor = null;
|
||||
if (restoreFocus && modeId) {
|
||||
$(`[data-mode-edit="${CSS.escape(modeId)}"]`)?.focus();
|
||||
}
|
||||
if (restoreFocus && restoreSelector) $(restoreSelector)?.focus();
|
||||
}
|
||||
|
||||
function invalidateModeEditorFlow() {
|
||||
@@ -5373,7 +5477,7 @@ async function executeModeEditorInstruction() {
|
||||
const execute = $("#modeEditorExecute");
|
||||
execute.disabled = true;
|
||||
execute.classList.add("loading");
|
||||
execute.innerHTML = '<i data-lucide="loader-circle"></i><span>生成中…</span>';
|
||||
execute.innerHTML = '<i data-lucide="loader-circle" data-icon="inline-start"></i><span>生成中…</span>';
|
||||
$("#modeEditorComplete").disabled = true;
|
||||
$("#modeEditorStatus").textContent = "正在复用主页解析流程生成控制指令;不会执行设备…";
|
||||
refreshIcons();
|
||||
@@ -5403,7 +5507,7 @@ async function executeModeEditorInstruction() {
|
||||
} finally {
|
||||
execute.disabled = false;
|
||||
execute.classList.remove("loading");
|
||||
execute.innerHTML = '<i data-lucide="wand-sparkles"></i><span>生成指令</span>';
|
||||
execute.innerHTML = '<i data-lucide="wand-sparkles" data-icon="inline-start"></i><span>生成指令</span>';
|
||||
refreshIcons();
|
||||
}
|
||||
}
|
||||
@@ -5417,6 +5521,12 @@ async function completeModeEditor() {
|
||||
$("#modeEditorName").focus();
|
||||
return;
|
||||
}
|
||||
if (editor.isNew && !name.endsWith("模式") && name.length > 62) {
|
||||
showToast("模式名称添加“模式”后不能超过 64 个字符");
|
||||
$("#modeEditorName").focus();
|
||||
return;
|
||||
}
|
||||
const modeName = editor.isNew && !name.endsWith("模式") ? `${name}模式` : name;
|
||||
if (
|
||||
!editor.sourceRequestId
|
||||
|| editor.executedInstruction !== instruction
|
||||
@@ -5428,14 +5538,17 @@ async function completeModeEditor() {
|
||||
const complete = $("#modeEditorComplete");
|
||||
complete.disabled = true;
|
||||
complete.classList.add("loading");
|
||||
complete.innerHTML = '<i data-lucide="loader-circle"></i><span>保存中…</span>';
|
||||
complete.innerHTML = '<i data-lucide="loader-circle" data-icon="inline-start"></i><span>保存中…</span>';
|
||||
refreshIcons();
|
||||
try {
|
||||
const response = await apiRequest(`/api/mode-profiles/${encodeURIComponent(editor.modeId)}`, {
|
||||
const endpoint = editor.isNew
|
||||
? "/api/mode-profiles"
|
||||
: `/api/mode-profiles/${encodeURIComponent(editor.modeId)}`;
|
||||
const response = await apiRequest(endpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
name: modeName,
|
||||
instruction,
|
||||
source_request_id: editor.sourceRequestId,
|
||||
vehicle_id: state.vehicleId || state.runtime?.default_vehicle_id,
|
||||
@@ -5449,14 +5562,16 @@ async function completeModeEditor() {
|
||||
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
|
||||
await loadRuntime();
|
||||
closeModeEditor({ restoreFocus: false });
|
||||
showToast(`${payload.mode_profile?.name || name}已永久保存到模式卡片`);
|
||||
showToast(`${payload.mode_profile?.name || modeName}已永久保存到模式卡片`);
|
||||
} catch (error) {
|
||||
$("#modeEditorStatus").textContent = error.message || "模式保存失败。";
|
||||
showToast(error.message || "模式保存失败");
|
||||
renderModeEditorFlow();
|
||||
} finally {
|
||||
complete.classList.remove("loading");
|
||||
complete.innerHTML = '<i data-lucide="save"></i><span>完成</span>';
|
||||
complete.innerHTML = editor.isNew
|
||||
? '<i data-lucide="bookmark-plus" data-icon="inline-start"></i><span>添加模式</span>'
|
||||
: '<i data-lucide="save" data-icon="inline-start"></i><span>完成</span>';
|
||||
refreshIcons();
|
||||
}
|
||||
}
|
||||
@@ -5573,10 +5688,8 @@ function populateModeChips() {
|
||||
group.append(divider);
|
||||
}
|
||||
dynamicButtons.forEach((button) => group.append(button));
|
||||
if (userButtons.length) group.scrollLeft = 0;
|
||||
populateModeGallery();
|
||||
refreshIcons();
|
||||
window.requestAnimationFrame(updateModeRailNavigation);
|
||||
}
|
||||
|
||||
async function deleteUserModeProfile(modeProfileId) {
|
||||
@@ -5724,6 +5837,8 @@ function applyModeCommand(command, modeProfileId = null) {
|
||||
return;
|
||||
}
|
||||
const mode = modeProfiles().find((item) => item.id === modeProfileId);
|
||||
setModeRailSelection(mode);
|
||||
setModeRailExpanded(false);
|
||||
if (mode?.command_flow?.source_request_id) {
|
||||
void executeSavedModeProfile(mode);
|
||||
return;
|
||||
@@ -5735,99 +5850,6 @@ function applyModeCommand(command, modeProfileId = null) {
|
||||
$("#requestForm").requestSubmit();
|
||||
}
|
||||
|
||||
function updateModeDraftPreview() {
|
||||
const command = $("#utterance").value.trim();
|
||||
state.modeDraftCommand = command;
|
||||
const source = $("#saveModeSource");
|
||||
source.textContent = command || "请先在指令框输入需要固化的功能组合";
|
||||
source.classList.toggle("empty", !command);
|
||||
}
|
||||
|
||||
function toggleSaveModeComposer() {
|
||||
const composer = $("#saveModeComposer");
|
||||
composer.hidden = !composer.hidden;
|
||||
if (!composer.hidden) {
|
||||
updateModeDraftPreview();
|
||||
$("#saveModeName").focus();
|
||||
}
|
||||
}
|
||||
|
||||
function closeSaveModeComposer() {
|
||||
$("#saveModeComposer").hidden = true;
|
||||
state.modeDraftCommand = "";
|
||||
}
|
||||
|
||||
function requestCommandAndWait(utterance) {
|
||||
return new Promise((resolve) => {
|
||||
const button = $("#runButton");
|
||||
if (!button || button.disabled) {
|
||||
resolve({ result: null, error: { message: "上一条指令仍在执行" } });
|
||||
return;
|
||||
}
|
||||
const timeoutMs = requestTimeoutForExecutionMode(state.executionMode) + 5000;
|
||||
const onComplete = (event) => {
|
||||
if (event.detail?.utterance !== utterance) return;
|
||||
window.clearTimeout(timer);
|
||||
window.removeEventListener("cognitive-agent:request-complete", onComplete);
|
||||
resolve(event.detail);
|
||||
};
|
||||
const timer = window.setTimeout(() => {
|
||||
window.removeEventListener("cognitive-agent:request-complete", onComplete);
|
||||
resolve({ result: null, error: { message: "指令执行等待超时" } });
|
||||
}, timeoutMs);
|
||||
window.addEventListener("cognitive-agent:request-complete", onComplete);
|
||||
$("#utterance").value = utterance;
|
||||
$("#requestForm").requestSubmit();
|
||||
});
|
||||
}
|
||||
|
||||
async function saveCurrentModeProfile() {
|
||||
const input = $("#saveModeName");
|
||||
const rawName = input.value.trim();
|
||||
if (!rawName) {
|
||||
showToast("请输入模式名称");
|
||||
return;
|
||||
}
|
||||
const sourceCommand = state.modeDraftCommand || $("#utterance").value.trim();
|
||||
if (!sourceCommand) {
|
||||
showToast("请先输入需要创建为模式的用户指令");
|
||||
$("#utterance").focus();
|
||||
return;
|
||||
}
|
||||
const modeName = rawName.endsWith("模式") ? rawName : `${rawName}模式`;
|
||||
const confirm = $("#saveModeConfirm");
|
||||
confirm.disabled = true;
|
||||
confirm.classList.add("loading");
|
||||
confirm.textContent = "创建中…";
|
||||
try {
|
||||
if (state.lastSuccessfulUtterance !== sourceCommand) {
|
||||
showToast("正在执行当前指令,成功后将创建功能模式");
|
||||
const execution = await requestCommandAndWait(sourceCommand);
|
||||
if (!execution.result || !["succeeded", "partially_succeeded"].includes(execution.result.status)) {
|
||||
throw new Error(execution.error?.message || "当前指令未成功执行,未创建功能模式");
|
||||
}
|
||||
}
|
||||
const saveCommand = `这个模式不错,固化为${modeName}`;
|
||||
const saved = await requestCommandAndWait(saveCommand);
|
||||
if (!saved.result?.mode_profile || saved.result.status !== "succeeded") {
|
||||
throw new Error(saved.error?.message || "功能模式创建失败");
|
||||
}
|
||||
await loadRuntime();
|
||||
const createdModeId = saved.result.mode_profile.id;
|
||||
setModeExecutionState(createdModeId, "succeeded");
|
||||
closeSaveModeComposer();
|
||||
input.value = "";
|
||||
$("#utterance").value = sourceCommand;
|
||||
showToast(`${saved.result.mode_profile.name}已创建并添加到功能模式栏`);
|
||||
} catch (error) {
|
||||
showToast(error.message || "功能模式创建失败");
|
||||
} finally {
|
||||
confirm.disabled = false;
|
||||
confirm.classList.remove("loading");
|
||||
confirm.textContent = "创建并添加";
|
||||
}
|
||||
}
|
||||
|
||||
function applyRuntimeTargetPolicy(runtime) {
|
||||
const selectedTransport = selectedVehicleTransport();
|
||||
const wirelessDualChannel = state.controlTransport === "wireless_mqtt"
|
||||
@@ -5903,18 +5925,14 @@ async function loadRuntime() {
|
||||
syncReasoningControls();
|
||||
$("#matrixCount").textContent = `${state.runtime.matrix.messages} / ${state.runtime.matrix.signals}`;
|
||||
const hardwareButton = $("#hardwareMode");
|
||||
// “真实台架”是操作员手动选择的执行目标。硬件暂不可用时仍允许
|
||||
// “真实下发”是操作员手动选择的执行目标。硬件暂不可用时仍允许
|
||||
// 选择该目标并展示诊断信息;真正的控制提交继续由后端安全门拦截。
|
||||
hardwareButton.disabled = false;
|
||||
hardwareButton.dataset.executionState = hardwareExecutionReady ? "available" : "blocked";
|
||||
hardwareButton.title = hardwareExecutionReady
|
||||
? "真实台架已通过后端执行配置校验"
|
||||
: `可手动选择真实台架;当前不可执行:${hardwareExecutionIssue(hardware, vehicleCan)}`;
|
||||
? "真实下发已通过后端执行配置校验"
|
||||
: `可手动选择真实下发;当前不可执行:${hardwareExecutionIssue(hardware, vehicleCan)}`;
|
||||
const simulation = state.runtime.simulated_hardware || {};
|
||||
$("#simulationMode").disabled = !simulation.enabled;
|
||||
$("#simulationMode").title = simulation.enabled
|
||||
? "开发专用模拟台架,不连接物理 CAN"
|
||||
: "通过安卓联调脚本或环境变量启用模拟台架";
|
||||
const profileText = (hardware.firmware_profiles || []).join(" / ") || "未选择 profile";
|
||||
const flashText = hardware.auto_flash_enabled ? "自动烧录已启用" : "自动烧录未启用";
|
||||
const concurrencyText = hardware.concurrency_policy === "single_flight_fail_fast"
|
||||
@@ -5933,7 +5951,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}`
|
||||
: `可手动选择真实台架;当前不可执行:${hardwareExecutionIssue(hardware, vehicleCan)}`;
|
||||
: `可手动选择真实下发;当前不可执行:${hardwareExecutionIssue(hardware, vehicleCan)}`;
|
||||
$("#hardwareAliasNote").textContent = ch1DriverOnly && vehicleHardwareReady && !seatHardwareReady
|
||||
? `${mqttSelected ? "MQTT" : "CH1"} → 车身 CAN / 主驾座椅 · CH0 与副驾 T2G 路径禁止`
|
||||
: linkedHardwareReady
|
||||
@@ -5967,7 +5985,11 @@ async function synchronizePassengerSeatState() {
|
||||
const stateReady = mqttSelected
|
||||
? selectedVehicleTransport()?.state_configured
|
||||
: state.runtime?.hardware?.configured;
|
||||
if (!stateReady) return false;
|
||||
if (!stateReady) {
|
||||
updateWiredCanProbe("passenger", false, requestedTransport);
|
||||
setFeedbackSignal("passenger", false, requestedTransport);
|
||||
return false;
|
||||
}
|
||||
const query = new URLSearchParams({
|
||||
conversation_id: state.conversationId,
|
||||
vehicle_id: state.vehicleId || state.runtime.default_vehicle_id || "",
|
||||
@@ -5985,6 +6007,8 @@ async function synchronizePassengerSeatState() {
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const payload = await response.json();
|
||||
if (requestedTransport !== state.controlTransport) return false;
|
||||
updateWiredCanProbe("passenger", true, requestedTransport);
|
||||
setFeedbackSignal("passenger", true, requestedTransport);
|
||||
renderSeatState({ passenger: payload.actuator_state?.passenger || {} });
|
||||
state.seatFeedbackSources.passenger = payload.complete
|
||||
? payload.source === "wireless_mqtt_ch0" ? "wireless_mqtt_ch0" : "physical_can"
|
||||
@@ -5993,6 +6017,8 @@ async function synchronizePassengerSeatState() {
|
||||
updateSeatFeedbackModeLabel();
|
||||
return true;
|
||||
} catch (_error) {
|
||||
updateWiredCanProbe("passenger", false, requestedTransport);
|
||||
setFeedbackSignal("passenger", false, requestedTransport);
|
||||
if (!["physical_can", "wireless_mqtt_ch0"].includes(state.seatFeedbackSources.passenger)) {
|
||||
state.seatFeedbackSources.passenger = "unavailable";
|
||||
updateSeatFeedbackModeLabel();
|
||||
@@ -6002,8 +6028,12 @@ async function synchronizePassengerSeatState() {
|
||||
}
|
||||
|
||||
async function synchronizeDriverSeatState() {
|
||||
if (!selectedVehicleTransport()?.state_configured) return false;
|
||||
const requestedTransport = state.controlTransport;
|
||||
if (!selectedVehicleTransport()?.state_configured) {
|
||||
updateWiredCanProbe("vehicle", false, requestedTransport);
|
||||
setFeedbackSignal("vehicle", false, requestedTransport);
|
||||
return false;
|
||||
}
|
||||
const query = new URLSearchParams({
|
||||
conversation_id: state.conversationId,
|
||||
vehicle_id: state.vehicleId || state.runtime.default_vehicle_id || "",
|
||||
@@ -6019,6 +6049,12 @@ async function synchronizeDriverSeatState() {
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const payload = await response.json();
|
||||
if (requestedTransport !== state.controlTransport) return false;
|
||||
updateWiredCanProbe("vehicle", true, requestedTransport);
|
||||
setFeedbackSignal(
|
||||
"vehicle",
|
||||
Array.isArray(payload.observed_frame_ids) && payload.observed_frame_ids.length > 0,
|
||||
requestedTransport,
|
||||
);
|
||||
applyAuthoritativeVehicleState(payload);
|
||||
state.seatFeedbackSources.driver = payload.complete
|
||||
? payload.source === "wireless_mqtt_ch1" ? "wireless_mqtt_ch1" : "physical_can_ch1"
|
||||
@@ -6027,6 +6063,8 @@ async function synchronizeDriverSeatState() {
|
||||
updateSeatFeedbackModeLabel();
|
||||
return true;
|
||||
} catch (error) {
|
||||
updateWiredCanProbe("vehicle", false, requestedTransport);
|
||||
setFeedbackSignal("vehicle", false, requestedTransport);
|
||||
state.vehicleStateSnapshot = {
|
||||
...(state.vehicleStateSnapshot || {}),
|
||||
error: error.message || String(error),
|
||||
@@ -7816,7 +7854,6 @@ async function submitRequest(event) {
|
||||
await synchronizePhysicalSeatState();
|
||||
}
|
||||
const completed = ["succeeded", "partially_succeeded"].includes(payload.status);
|
||||
if (completed) state.lastSuccessfulUtterance = utterance;
|
||||
if (submittedModeProfileId) {
|
||||
setModeExecutionState(submittedModeProfileId, completed ? "succeeded" : "failed");
|
||||
}
|
||||
@@ -8005,13 +8042,21 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
button.dataset.modeProfileId || null,
|
||||
);
|
||||
});
|
||||
$(".mode-chips").addEventListener(
|
||||
"scroll",
|
||||
() => window.requestAnimationFrame(updateModeRailNavigation),
|
||||
{ passive: true },
|
||||
);
|
||||
$("#modeRailPrev").addEventListener("click", () => scrollModeRail(-1));
|
||||
$("#modeRailNext").addEventListener("click", () => scrollModeRail(1));
|
||||
$("#modeRailToggle").addEventListener("click", () => {
|
||||
setModeRailExpanded($("#modeRailToggle").getAttribute("aria-expanded") !== "true");
|
||||
});
|
||||
document.addEventListener("pointerdown", (event) => {
|
||||
const rail = $(".mode-rail");
|
||||
const target = event.target instanceof Element ? event.target : null;
|
||||
if (
|
||||
rail
|
||||
&& target
|
||||
&& !rail.contains(target)
|
||||
&& $("#modeRailToggle").getAttribute("aria-expanded") === "true"
|
||||
) {
|
||||
setModeRailExpanded(false);
|
||||
}
|
||||
});
|
||||
$("#modeGalleryToggle").addEventListener("click", openModeGallery);
|
||||
$("#modeGalleryClose").addEventListener("click", () => closeModeGallery());
|
||||
$("#modeGalleryBackdrop").addEventListener("click", (event) => {
|
||||
@@ -8019,6 +8064,12 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
});
|
||||
$("#modeGalleryGroups").addEventListener("click", (event) => {
|
||||
const target = event.target instanceof Element ? event.target : null;
|
||||
const createButton = target?.closest("button[data-mode-create]");
|
||||
if (createButton) {
|
||||
event.preventDefault();
|
||||
openModeEditorForCreate({ restoreSelector: "[data-mode-create]" });
|
||||
return;
|
||||
}
|
||||
const editButton = target?.closest("button[data-mode-edit]");
|
||||
if (editButton) {
|
||||
event.preventDefault();
|
||||
@@ -8059,21 +8110,13 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
} else if (event.key === "Escape" && !$("#modeGalleryBackdrop").hidden) {
|
||||
event.preventDefault();
|
||||
closeModeGallery();
|
||||
}
|
||||
});
|
||||
window.addEventListener("resize", () => window.requestAnimationFrame(updateModeRailNavigation));
|
||||
$("#utterance").addEventListener("input", () => {
|
||||
if (!$("#saveModeComposer").hidden) updateModeDraftPreview();
|
||||
});
|
||||
$("#saveModeToggle").addEventListener("click", toggleSaveModeComposer);
|
||||
$("#saveModeCancel").addEventListener("click", closeSaveModeComposer);
|
||||
$("#saveModeConfirm").addEventListener("click", saveCurrentModeProfile);
|
||||
$("#saveModeName").addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" && !event.isComposing) {
|
||||
} else if (event.key === "Escape" && $("#modeRailToggle").getAttribute("aria-expanded") === "true") {
|
||||
event.preventDefault();
|
||||
saveCurrentModeProfile();
|
||||
setModeRailExpanded(false);
|
||||
$("#modeRailToggle").focus();
|
||||
}
|
||||
});
|
||||
$("#saveModeToggle").addEventListener("click", () => openModeEditorForCreate());
|
||||
$$(".segment[data-provider]").forEach((button) => button.addEventListener("click", () => selectProvider(button.dataset.provider)));
|
||||
$$('[data-reasoning-effort]').forEach((button) => button.addEventListener("click", () => selectReasoningEffort(button.dataset.reasoningEffort)));
|
||||
$$('[data-execution-mode]').forEach((button) => button.addEventListener("click", () => selectExecutionMode(button.dataset.executionMode)));
|
||||
|
||||
Reference in New Issue
Block a user