优化后端和前端逻辑
This commit is contained in:
+222
-179
@@ -70,10 +70,10 @@ const state = {
|
|||||||
physicalStatePoller: null,
|
physicalStatePoller: null,
|
||||||
physicalStateSyncPromise: null,
|
physicalStateSyncPromise: null,
|
||||||
passengerStateSyncPromise: null,
|
passengerStateSyncPromise: null,
|
||||||
|
wiredCanProbe: { passenger: false, vehicle: false },
|
||||||
|
feedbackSignals: { passenger: false, vehicle: false },
|
||||||
pendingModeProfileId: null,
|
pendingModeProfileId: null,
|
||||||
modeRunTimer: null,
|
modeRunTimer: null,
|
||||||
modeDraftCommand: "",
|
|
||||||
lastSuccessfulUtterance: "",
|
|
||||||
modeEditor: null,
|
modeEditor: null,
|
||||||
directCanCommandPending: false,
|
directCanCommandPending: false,
|
||||||
};
|
};
|
||||||
@@ -133,6 +133,13 @@ function renderCabinLiveStatus(networkStatus = state.connectionStatus) {
|
|||||||
setSummarySystem("链路未配置", "waiting");
|
setSummarySystem("链路未配置", "waiting");
|
||||||
return;
|
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 snapshot = state.vehicleStateSnapshot;
|
||||||
const observedIds = Array.isArray(snapshot?.observedFrameIds)
|
const observedIds = Array.isArray(snapshot?.observedFrameIds)
|
||||||
@@ -4323,6 +4330,54 @@ function selectedVehicleTransport() {
|
|||||||
|| null;
|
|| 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() {
|
function isHardwareExecutionReady() {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
state.runtime?.hardware?.configured || selectedVehicleTransport()?.configured,
|
state.runtime?.hardware?.configured || selectedVehicleTransport()?.configured,
|
||||||
@@ -4370,9 +4425,14 @@ function renderControlTransport() {
|
|||||||
// shown as offline, but it must never disable or silently change the
|
// shown as offline, but it must never disable or silently change the
|
||||||
// operator's wired/wireless selection.
|
// operator's wired/wireless selection.
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
button.dataset.connectionState = ["online", "ready"].includes(runtime?.state_sync?.state)
|
const connectionState = transportConnectionOnline(id, runtime)
|
||||||
? "online"
|
? "online"
|
||||||
: "offline";
|
: "offline";
|
||||||
|
button.dataset.connectionState = connectionState;
|
||||||
|
button.setAttribute(
|
||||||
|
"aria-label",
|
||||||
|
`${id === "wireless_mqtt" ? "无线 CAN" : "有线 CAN"},信号链路${connectionState === "online" ? "正常" : "断开"}`,
|
||||||
|
);
|
||||||
button.title = runtime?.state_configured === true
|
button.title = runtime?.state_configured === true
|
||||||
? runtime?.state_sync?.last_error || ""
|
? runtime?.state_sync?.last_error || ""
|
||||||
: `${id === "wireless_mqtt" ? "无线 MQTT" : "有线 CAN"} 当前未配置(仍可手动选择)`;
|
: `${id === "wireless_mqtt" ? "无线 MQTT" : "有线 CAN"} 当前未配置(仍可手动选择)`;
|
||||||
@@ -4388,14 +4448,10 @@ function renderControlTransport() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.controlTransport === "wireless_mqtt") {
|
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"}`;
|
note.textContent = `MQTT ${connection} · 状态 ${selected?.condition_topic || "n7/n7-headunit/can/state"} · 控制 ${selected?.control_topic || "n7/n7-headunit/can/command"}`;
|
||||||
} else {
|
} else {
|
||||||
const connection = selected?.state_sync?.state === "online"
|
const connection = transportConnectionOnline("wired_can", selected) ? "已连接" : "未连接";
|
||||||
? "已连接"
|
|
||||||
: selected?.state_sync?.state === "ready"
|
|
||||||
? "已配置"
|
|
||||||
: "未连接";
|
|
||||||
const dualChannel = selected?.target_policy
|
const dualChannel = selected?.target_policy
|
||||||
=== "dual_channel_wired_can0_passenger_can1_vehicle";
|
=== "dual_channel_wired_can0_passenger_can1_vehicle";
|
||||||
note.textContent = dualChannel
|
note.textContent = dualChannel
|
||||||
@@ -4414,6 +4470,7 @@ function selectControlTransport(transport) {
|
|||||||
sessionStorage.setItem("cognitive-agent.conversation-id", state.conversationId);
|
sessionStorage.setItem("cognitive-agent.conversation-id", state.conversationId);
|
||||||
state.seatFeedbackSources.passenger = "unavailable";
|
state.seatFeedbackSources.passenger = "unavailable";
|
||||||
state.seatFeedbackSources.driver = "unavailable";
|
state.seatFeedbackSources.driver = "unavailable";
|
||||||
|
resetTransportConnectionEvidence();
|
||||||
resetVehicleStatusDisplay();
|
resetVehicleStatusDisplay();
|
||||||
}
|
}
|
||||||
state.vehicleStateSnapshot = null;
|
state.vehicleStateSnapshot = null;
|
||||||
@@ -4510,7 +4567,7 @@ function selectExecutionMode(mode, { remember = true } = {}) {
|
|||||||
: simulatedHardware ? "运行故障仿真" : "生成并执行";
|
: simulatedHardware ? "运行故障仿真" : "生成并执行";
|
||||||
}
|
}
|
||||||
if (remember && mode === "hardware" && !hardwareExecutionReady) {
|
if (remember && mode === "hardware" && !hardwareExecutionReady) {
|
||||||
showToast(`已切换到真实台架;当前不可执行:${hardwareExecutionIssue()}`);
|
showToast(`已切换到真实下发;当前不可执行:${hardwareExecutionIssue()}`);
|
||||||
}
|
}
|
||||||
updateSeatFeedbackModeLabel();
|
updateSeatFeedbackModeLabel();
|
||||||
refreshIcons();
|
refreshIcons();
|
||||||
@@ -4811,6 +4868,43 @@ function createModeGalleryCard(mode) {
|
|||||||
return shell;
|
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() {
|
function populateModeGallery() {
|
||||||
const container = $("#modeGalleryGroups");
|
const container = $("#modeGalleryGroups");
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -4837,14 +4931,8 @@ function populateModeGallery() {
|
|||||||
|
|
||||||
const grid = document.createElement("div");
|
const grid = document.createElement("div");
|
||||||
grid.className = "mode-gallery-grid";
|
grid.className = "mode-gallery-grid";
|
||||||
if (group.modes.length) {
|
if (group.id === "personal") grid.append(createModeGalleryAddCard());
|
||||||
group.modes.forEach((mode) => grid.append(createModeGalleryCard(mode)));
|
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);
|
|
||||||
}
|
|
||||||
section.append(grid);
|
section.append(grid);
|
||||||
container.append(section);
|
container.append(section);
|
||||||
});
|
});
|
||||||
@@ -4854,38 +4942,24 @@ function populateModeGallery() {
|
|||||||
refreshIcons();
|
refreshIcons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateModeRailNavigation() {
|
function setModeRailExpanded(expanded) {
|
||||||
const chips = $(".mode-chips");
|
const toggle = $("#modeRailToggle");
|
||||||
const previous = $("#modeRailPrev");
|
const content = $("#modeRailContent");
|
||||||
const next = $("#modeRailNext");
|
if (!toggle || !content) return;
|
||||||
if (!chips || !previous || !next) return;
|
const open = expanded === true;
|
||||||
const maxScroll = Math.max(0, chips.scrollWidth - chips.clientWidth);
|
toggle.setAttribute("aria-expanded", String(open));
|
||||||
previous.disabled = maxScroll <= 2 || chips.scrollLeft <= 2;
|
content.hidden = !open;
|
||||||
next.disabled = maxScroll <= 2 || chips.scrollLeft >= maxScroll - 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollModeRail(direction) {
|
function setModeRailSelection(mode) {
|
||||||
const chips = $(".mode-chips");
|
const selection = $("#modeRailSelection");
|
||||||
if (!chips) return;
|
if (selection && mode) selection.textContent = String(mode.name || mode.id || "常用模式");
|
||||||
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 openModeGallery() {
|
function openModeGallery() {
|
||||||
const backdrop = $("#modeGalleryBackdrop");
|
const backdrop = $("#modeGalleryBackdrop");
|
||||||
if (!backdrop) return;
|
if (!backdrop) return;
|
||||||
closeSaveModeComposer();
|
setModeRailExpanded(false);
|
||||||
backdrop.hidden = false;
|
backdrop.hidden = false;
|
||||||
document.body.classList.add("mode-gallery-open");
|
document.body.classList.add("mode-gallery-open");
|
||||||
$("#modeGalleryToggle").setAttribute("aria-expanded", "true");
|
$("#modeGalleryToggle").setAttribute("aria-expanded", "true");
|
||||||
@@ -5318,12 +5392,16 @@ function openModeEditor(modeProfileId) {
|
|||||||
: null;
|
: null;
|
||||||
state.modeEditor = {
|
state.modeEditor = {
|
||||||
modeId: mode.id,
|
modeId: mode.id,
|
||||||
|
isNew: false,
|
||||||
sourceRequestId: String(flow?.source_request_id || ""),
|
sourceRequestId: String(flow?.source_request_id || ""),
|
||||||
executedInstruction: flow ? instruction : "",
|
executedInstruction: flow ? instruction : "",
|
||||||
flow,
|
flow,
|
||||||
|
restoreSelector: `[data-mode-edit="${CSS.escape(mode.id)}"]`,
|
||||||
};
|
};
|
||||||
|
$("#modeEditorTitle").textContent = "编辑功能模式";
|
||||||
$("#modeEditorName").value = mode.name || "";
|
$("#modeEditorName").value = mode.name || "";
|
||||||
$("#modeEditorInstruction").value = instruction;
|
$("#modeEditorInstruction").value = instruction;
|
||||||
|
$("#modeEditorComplete").innerHTML = '<i data-lucide="save" data-icon="inline-start"></i><span>完成</span>';
|
||||||
$("#modeEditorStatus").textContent = flow
|
$("#modeEditorStatus").textContent = flow
|
||||||
? "已载入永久保存的自然语言和控制指令流,可修改后重新生成。"
|
? "已载入永久保存的自然语言和控制指令流,可修改后重新生成。"
|
||||||
: "修改自然语言后需要重新生成指令,才能点击完成永久保存。";
|
: "修改自然语言后需要重新生成指令,才能点击完成永久保存。";
|
||||||
@@ -5338,18 +5416,44 @@ function openModeEditor(modeProfileId) {
|
|||||||
refreshIcons();
|
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 } = {}) {
|
function closeModeEditor({ restoreFocus = true } = {}) {
|
||||||
const backdrop = $("#modeEditorBackdrop");
|
const backdrop = $("#modeEditorBackdrop");
|
||||||
const modeId = state.modeEditor?.modeId;
|
const restoreSelector = state.modeEditor?.restoreSelector;
|
||||||
if (!backdrop || backdrop.hidden) return;
|
if (!backdrop || backdrop.hidden) return;
|
||||||
backdrop.classList.remove("open");
|
backdrop.classList.remove("open");
|
||||||
backdrop.hidden = true;
|
backdrop.hidden = true;
|
||||||
setModeEditorFlowExpanded(false);
|
setModeEditorFlowExpanded(false);
|
||||||
document.body.classList.remove("mode-editor-open");
|
document.body.classList.remove("mode-editor-open");
|
||||||
state.modeEditor = null;
|
state.modeEditor = null;
|
||||||
if (restoreFocus && modeId) {
|
if (restoreFocus && restoreSelector) $(restoreSelector)?.focus();
|
||||||
$(`[data-mode-edit="${CSS.escape(modeId)}"]`)?.focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function invalidateModeEditorFlow() {
|
function invalidateModeEditorFlow() {
|
||||||
@@ -5373,7 +5477,7 @@ async function executeModeEditorInstruction() {
|
|||||||
const execute = $("#modeEditorExecute");
|
const execute = $("#modeEditorExecute");
|
||||||
execute.disabled = true;
|
execute.disabled = true;
|
||||||
execute.classList.add("loading");
|
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;
|
$("#modeEditorComplete").disabled = true;
|
||||||
$("#modeEditorStatus").textContent = "正在复用主页解析流程生成控制指令;不会执行设备…";
|
$("#modeEditorStatus").textContent = "正在复用主页解析流程生成控制指令;不会执行设备…";
|
||||||
refreshIcons();
|
refreshIcons();
|
||||||
@@ -5403,7 +5507,7 @@ async function executeModeEditorInstruction() {
|
|||||||
} finally {
|
} finally {
|
||||||
execute.disabled = false;
|
execute.disabled = false;
|
||||||
execute.classList.remove("loading");
|
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();
|
refreshIcons();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5417,6 +5521,12 @@ async function completeModeEditor() {
|
|||||||
$("#modeEditorName").focus();
|
$("#modeEditorName").focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (editor.isNew && !name.endsWith("模式") && name.length > 62) {
|
||||||
|
showToast("模式名称添加“模式”后不能超过 64 个字符");
|
||||||
|
$("#modeEditorName").focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const modeName = editor.isNew && !name.endsWith("模式") ? `${name}模式` : name;
|
||||||
if (
|
if (
|
||||||
!editor.sourceRequestId
|
!editor.sourceRequestId
|
||||||
|| editor.executedInstruction !== instruction
|
|| editor.executedInstruction !== instruction
|
||||||
@@ -5428,14 +5538,17 @@ async function completeModeEditor() {
|
|||||||
const complete = $("#modeEditorComplete");
|
const complete = $("#modeEditorComplete");
|
||||||
complete.disabled = true;
|
complete.disabled = true;
|
||||||
complete.classList.add("loading");
|
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();
|
refreshIcons();
|
||||||
try {
|
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",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name,
|
name: modeName,
|
||||||
instruction,
|
instruction,
|
||||||
source_request_id: editor.sourceRequestId,
|
source_request_id: editor.sourceRequestId,
|
||||||
vehicle_id: state.vehicleId || state.runtime?.default_vehicle_id,
|
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}`);
|
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
|
||||||
await loadRuntime();
|
await loadRuntime();
|
||||||
closeModeEditor({ restoreFocus: false });
|
closeModeEditor({ restoreFocus: false });
|
||||||
showToast(`${payload.mode_profile?.name || name}已永久保存到模式卡片`);
|
showToast(`${payload.mode_profile?.name || modeName}已永久保存到模式卡片`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
$("#modeEditorStatus").textContent = error.message || "模式保存失败。";
|
$("#modeEditorStatus").textContent = error.message || "模式保存失败。";
|
||||||
showToast(error.message || "模式保存失败");
|
showToast(error.message || "模式保存失败");
|
||||||
renderModeEditorFlow();
|
renderModeEditorFlow();
|
||||||
} finally {
|
} finally {
|
||||||
complete.classList.remove("loading");
|
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();
|
refreshIcons();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5573,10 +5688,8 @@ function populateModeChips() {
|
|||||||
group.append(divider);
|
group.append(divider);
|
||||||
}
|
}
|
||||||
dynamicButtons.forEach((button) => group.append(button));
|
dynamicButtons.forEach((button) => group.append(button));
|
||||||
if (userButtons.length) group.scrollLeft = 0;
|
|
||||||
populateModeGallery();
|
populateModeGallery();
|
||||||
refreshIcons();
|
refreshIcons();
|
||||||
window.requestAnimationFrame(updateModeRailNavigation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteUserModeProfile(modeProfileId) {
|
async function deleteUserModeProfile(modeProfileId) {
|
||||||
@@ -5724,6 +5837,8 @@ function applyModeCommand(command, modeProfileId = null) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const mode = modeProfiles().find((item) => item.id === modeProfileId);
|
const mode = modeProfiles().find((item) => item.id === modeProfileId);
|
||||||
|
setModeRailSelection(mode);
|
||||||
|
setModeRailExpanded(false);
|
||||||
if (mode?.command_flow?.source_request_id) {
|
if (mode?.command_flow?.source_request_id) {
|
||||||
void executeSavedModeProfile(mode);
|
void executeSavedModeProfile(mode);
|
||||||
return;
|
return;
|
||||||
@@ -5735,99 +5850,6 @@ function applyModeCommand(command, modeProfileId = null) {
|
|||||||
$("#requestForm").requestSubmit();
|
$("#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) {
|
function applyRuntimeTargetPolicy(runtime) {
|
||||||
const selectedTransport = selectedVehicleTransport();
|
const selectedTransport = selectedVehicleTransport();
|
||||||
const wirelessDualChannel = state.controlTransport === "wireless_mqtt"
|
const wirelessDualChannel = state.controlTransport === "wireless_mqtt"
|
||||||
@@ -5903,18 +5925,14 @@ async function loadRuntime() {
|
|||||||
syncReasoningControls();
|
syncReasoningControls();
|
||||||
$("#matrixCount").textContent = `${state.runtime.matrix.messages} / ${state.runtime.matrix.signals}`;
|
$("#matrixCount").textContent = `${state.runtime.matrix.messages} / ${state.runtime.matrix.signals}`;
|
||||||
const hardwareButton = $("#hardwareMode");
|
const hardwareButton = $("#hardwareMode");
|
||||||
// “真实台架”是操作员手动选择的执行目标。硬件暂不可用时仍允许
|
// “真实下发”是操作员手动选择的执行目标。硬件暂不可用时仍允许
|
||||||
// 选择该目标并展示诊断信息;真正的控制提交继续由后端安全门拦截。
|
// 选择该目标并展示诊断信息;真正的控制提交继续由后端安全门拦截。
|
||||||
hardwareButton.disabled = false;
|
hardwareButton.disabled = false;
|
||||||
hardwareButton.dataset.executionState = hardwareExecutionReady ? "available" : "blocked";
|
hardwareButton.dataset.executionState = hardwareExecutionReady ? "available" : "blocked";
|
||||||
hardwareButton.title = hardwareExecutionReady
|
hardwareButton.title = hardwareExecutionReady
|
||||||
? "真实台架已通过后端执行配置校验"
|
? "真实下发已通过后端执行配置校验"
|
||||||
: `可手动选择真实台架;当前不可执行:${hardwareExecutionIssue(hardware, vehicleCan)}`;
|
: `可手动选择真实下发;当前不可执行:${hardwareExecutionIssue(hardware, vehicleCan)}`;
|
||||||
const simulation = state.runtime.simulated_hardware || {};
|
const simulation = state.runtime.simulated_hardware || {};
|
||||||
$("#simulationMode").disabled = !simulation.enabled;
|
|
||||||
$("#simulationMode").title = simulation.enabled
|
|
||||||
? "开发专用模拟台架,不连接物理 CAN"
|
|
||||||
: "通过安卓联调脚本或环境变量启用模拟台架";
|
|
||||||
const profileText = (hardware.firmware_profiles || []).join(" / ") || "未选择 profile";
|
const profileText = (hardware.firmware_profiles || []).join(" / ") || "未选择 profile";
|
||||||
const flashText = hardware.auto_flash_enabled ? "自动烧录已启用" : "自动烧录未启用";
|
const flashText = hardware.auto_flash_enabled ? "自动烧录已启用" : "自动烧录未启用";
|
||||||
const concurrencyText = hardware.concurrency_policy === "single_flight_fail_fast"
|
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}`
|
? `${mqttDualChannel ? "MQTT CAN0 副驾座椅 · MQTT CAN1 其余车辆" : `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s`} · ${vehicleRouteText}${mqttDualChannel ? " · 远端自动烧录禁用" : ` · ${profileText} · ${flashText}`}${accessText}${concurrencyText}`
|
||||||
: seatHardwareReady
|
: seatHardwareReady
|
||||||
? `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s · ${profileText} · ${flashText} · 车端 CAN 未启用,其他领域保持虚拟执行${accessText}${concurrencyText}`
|
? `${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
|
$("#hardwareAliasNote").textContent = ch1DriverOnly && vehicleHardwareReady && !seatHardwareReady
|
||||||
? `${mqttSelected ? "MQTT" : "CH1"} → 车身 CAN / 主驾座椅 · CH0 与副驾 T2G 路径禁止`
|
? `${mqttSelected ? "MQTT" : "CH1"} → 车身 CAN / 主驾座椅 · CH0 与副驾 T2G 路径禁止`
|
||||||
: linkedHardwareReady
|
: linkedHardwareReady
|
||||||
@@ -5967,7 +5985,11 @@ async function synchronizePassengerSeatState() {
|
|||||||
const stateReady = mqttSelected
|
const stateReady = mqttSelected
|
||||||
? selectedVehicleTransport()?.state_configured
|
? selectedVehicleTransport()?.state_configured
|
||||||
: state.runtime?.hardware?.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({
|
const query = new URLSearchParams({
|
||||||
conversation_id: state.conversationId,
|
conversation_id: state.conversationId,
|
||||||
vehicle_id: state.vehicleId || state.runtime.default_vehicle_id || "",
|
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}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
const payload = await response.json();
|
const payload = await response.json();
|
||||||
if (requestedTransport !== state.controlTransport) return false;
|
if (requestedTransport !== state.controlTransport) return false;
|
||||||
|
updateWiredCanProbe("passenger", true, requestedTransport);
|
||||||
|
setFeedbackSignal("passenger", true, requestedTransport);
|
||||||
renderSeatState({ passenger: payload.actuator_state?.passenger || {} });
|
renderSeatState({ passenger: payload.actuator_state?.passenger || {} });
|
||||||
state.seatFeedbackSources.passenger = payload.complete
|
state.seatFeedbackSources.passenger = payload.complete
|
||||||
? payload.source === "wireless_mqtt_ch0" ? "wireless_mqtt_ch0" : "physical_can"
|
? payload.source === "wireless_mqtt_ch0" ? "wireless_mqtt_ch0" : "physical_can"
|
||||||
@@ -5993,6 +6017,8 @@ async function synchronizePassengerSeatState() {
|
|||||||
updateSeatFeedbackModeLabel();
|
updateSeatFeedbackModeLabel();
|
||||||
return true;
|
return true;
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
|
updateWiredCanProbe("passenger", false, requestedTransport);
|
||||||
|
setFeedbackSignal("passenger", false, requestedTransport);
|
||||||
if (!["physical_can", "wireless_mqtt_ch0"].includes(state.seatFeedbackSources.passenger)) {
|
if (!["physical_can", "wireless_mqtt_ch0"].includes(state.seatFeedbackSources.passenger)) {
|
||||||
state.seatFeedbackSources.passenger = "unavailable";
|
state.seatFeedbackSources.passenger = "unavailable";
|
||||||
updateSeatFeedbackModeLabel();
|
updateSeatFeedbackModeLabel();
|
||||||
@@ -6002,8 +6028,12 @@ async function synchronizePassengerSeatState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function synchronizeDriverSeatState() {
|
async function synchronizeDriverSeatState() {
|
||||||
if (!selectedVehicleTransport()?.state_configured) return false;
|
|
||||||
const requestedTransport = state.controlTransport;
|
const requestedTransport = state.controlTransport;
|
||||||
|
if (!selectedVehicleTransport()?.state_configured) {
|
||||||
|
updateWiredCanProbe("vehicle", false, requestedTransport);
|
||||||
|
setFeedbackSignal("vehicle", false, requestedTransport);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const query = new URLSearchParams({
|
const query = new URLSearchParams({
|
||||||
conversation_id: state.conversationId,
|
conversation_id: state.conversationId,
|
||||||
vehicle_id: state.vehicleId || state.runtime.default_vehicle_id || "",
|
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}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
const payload = await response.json();
|
const payload = await response.json();
|
||||||
if (requestedTransport !== state.controlTransport) return false;
|
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);
|
applyAuthoritativeVehicleState(payload);
|
||||||
state.seatFeedbackSources.driver = payload.complete
|
state.seatFeedbackSources.driver = payload.complete
|
||||||
? payload.source === "wireless_mqtt_ch1" ? "wireless_mqtt_ch1" : "physical_can_ch1"
|
? payload.source === "wireless_mqtt_ch1" ? "wireless_mqtt_ch1" : "physical_can_ch1"
|
||||||
@@ -6027,6 +6063,8 @@ async function synchronizeDriverSeatState() {
|
|||||||
updateSeatFeedbackModeLabel();
|
updateSeatFeedbackModeLabel();
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
updateWiredCanProbe("vehicle", false, requestedTransport);
|
||||||
|
setFeedbackSignal("vehicle", false, requestedTransport);
|
||||||
state.vehicleStateSnapshot = {
|
state.vehicleStateSnapshot = {
|
||||||
...(state.vehicleStateSnapshot || {}),
|
...(state.vehicleStateSnapshot || {}),
|
||||||
error: error.message || String(error),
|
error: error.message || String(error),
|
||||||
@@ -7816,7 +7854,6 @@ async function submitRequest(event) {
|
|||||||
await synchronizePhysicalSeatState();
|
await synchronizePhysicalSeatState();
|
||||||
}
|
}
|
||||||
const completed = ["succeeded", "partially_succeeded"].includes(payload.status);
|
const completed = ["succeeded", "partially_succeeded"].includes(payload.status);
|
||||||
if (completed) state.lastSuccessfulUtterance = utterance;
|
|
||||||
if (submittedModeProfileId) {
|
if (submittedModeProfileId) {
|
||||||
setModeExecutionState(submittedModeProfileId, completed ? "succeeded" : "failed");
|
setModeExecutionState(submittedModeProfileId, completed ? "succeeded" : "failed");
|
||||||
}
|
}
|
||||||
@@ -8005,13 +8042,21 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
button.dataset.modeProfileId || null,
|
button.dataset.modeProfileId || null,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
$(".mode-chips").addEventListener(
|
$("#modeRailToggle").addEventListener("click", () => {
|
||||||
"scroll",
|
setModeRailExpanded($("#modeRailToggle").getAttribute("aria-expanded") !== "true");
|
||||||
() => window.requestAnimationFrame(updateModeRailNavigation),
|
});
|
||||||
{ passive: true },
|
document.addEventListener("pointerdown", (event) => {
|
||||||
);
|
const rail = $(".mode-rail");
|
||||||
$("#modeRailPrev").addEventListener("click", () => scrollModeRail(-1));
|
const target = event.target instanceof Element ? event.target : null;
|
||||||
$("#modeRailNext").addEventListener("click", () => scrollModeRail(1));
|
if (
|
||||||
|
rail
|
||||||
|
&& target
|
||||||
|
&& !rail.contains(target)
|
||||||
|
&& $("#modeRailToggle").getAttribute("aria-expanded") === "true"
|
||||||
|
) {
|
||||||
|
setModeRailExpanded(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
$("#modeGalleryToggle").addEventListener("click", openModeGallery);
|
$("#modeGalleryToggle").addEventListener("click", openModeGallery);
|
||||||
$("#modeGalleryClose").addEventListener("click", () => closeModeGallery());
|
$("#modeGalleryClose").addEventListener("click", () => closeModeGallery());
|
||||||
$("#modeGalleryBackdrop").addEventListener("click", (event) => {
|
$("#modeGalleryBackdrop").addEventListener("click", (event) => {
|
||||||
@@ -8019,6 +8064,12 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
});
|
});
|
||||||
$("#modeGalleryGroups").addEventListener("click", (event) => {
|
$("#modeGalleryGroups").addEventListener("click", (event) => {
|
||||||
const target = event.target instanceof Element ? event.target : null;
|
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]");
|
const editButton = target?.closest("button[data-mode-edit]");
|
||||||
if (editButton) {
|
if (editButton) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -8059,21 +8110,13 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
} else if (event.key === "Escape" && !$("#modeGalleryBackdrop").hidden) {
|
} else if (event.key === "Escape" && !$("#modeGalleryBackdrop").hidden) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
closeModeGallery();
|
closeModeGallery();
|
||||||
}
|
} else if (event.key === "Escape" && $("#modeRailToggle").getAttribute("aria-expanded") === "true") {
|
||||||
});
|
|
||||||
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) {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
saveCurrentModeProfile();
|
setModeRailExpanded(false);
|
||||||
|
$("#modeRailToggle").focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
$("#saveModeToggle").addEventListener("click", () => openModeEditorForCreate());
|
||||||
$$(".segment[data-provider]").forEach((button) => button.addEventListener("click", () => selectProvider(button.dataset.provider)));
|
$$(".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-reasoning-effort]').forEach((button) => button.addEventListener("click", () => selectReasoningEffort(button.dataset.reasoningEffort)));
|
||||||
$$('[data-execution-mode]').forEach((button) => button.addEventListener("click", () => selectExecutionMode(button.dataset.executionMode)));
|
$$('[data-execution-mode]').forEach((button) => button.addEventListener("click", () => selectExecutionMode(button.dataset.executionMode)));
|
||||||
|
|||||||
+1250
-254
File diff suppressed because it is too large
Load Diff
@@ -806,6 +806,34 @@
|
|||||||
const selectControl = (key, label, value, options) => ({ key, label, type: "select", value, options });
|
const selectControl = (key, label, value, options) => ({ key, label, type: "select", value, options });
|
||||||
const hasControlValue = (values, key) => Object.hasOwn(values, key);
|
const hasControlValue = (values, key) => Object.hasOwn(values, key);
|
||||||
|
|
||||||
|
function normalizeDeviceReadout(value) {
|
||||||
|
const text = String(value ?? "").trim();
|
||||||
|
if (!text || /^-+$/.test(text) || /^--\s*/.test(text)) return "-";
|
||||||
|
if (/未知|等待反馈|同步中|状态未知/.test(text)) return "-";
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedTransportHasConnection() {
|
||||||
|
const selected = $('.vehicle-transport-options [data-control-transport][aria-pressed="true"]')
|
||||||
|
|| $('.vehicle-transport-options [data-control-transport].active');
|
||||||
|
return selected?.dataset.connectionState === "online";
|
||||||
|
}
|
||||||
|
|
||||||
|
function deviceFeedbackAvailable(deviceId) {
|
||||||
|
if (!selectedTransportHasConnection()) return false;
|
||||||
|
return deviceId === "passenger"
|
||||||
|
? document.body.dataset.passengerFeedbackState === "online"
|
||||||
|
: document.body.dataset.vehicleFeedbackState === "online";
|
||||||
|
}
|
||||||
|
|
||||||
|
function deviceReadoutRows(device) {
|
||||||
|
const rows = device.rows();
|
||||||
|
if (!deviceFeedbackAvailable(device.id)) {
|
||||||
|
return rows.map(([label]) => [label, "-"]);
|
||||||
|
}
|
||||||
|
return rows.map(([label, value]) => [label, normalizeDeviceReadout(value)]);
|
||||||
|
}
|
||||||
|
|
||||||
function deviceControlValue(deviceId, control) {
|
function deviceControlValue(deviceId, control) {
|
||||||
const draft = displaySync.deviceDrafts[deviceId];
|
const draft = displaySync.deviceDrafts[deviceId];
|
||||||
return draft && Object.hasOwn(draft, control.key) ? draft[control.key] : control.value;
|
return draft && Object.hasOwn(draft, control.key) ? draft[control.key] : control.value;
|
||||||
@@ -894,7 +922,9 @@
|
|||||||
rows: () => {
|
rows: () => {
|
||||||
const [frontBrightness, rearBrightness] = splitReadout("#ambientBrightness");
|
const [frontBrightness, rearBrightness] = splitReadout("#ambientBrightness");
|
||||||
const [frontColor, rearColor] = splitReadout("#ambientColorIndex");
|
const [frontColor, rearColor] = splitReadout("#ambientColorIndex");
|
||||||
return [["开关状态", /OFF|关闭/.test(readText("#ambientPower")) ? "关闭" : "开启"], ["前排亮度", frontBrightness], ["前排颜色", frontColor], ["后排亮度", rearBrightness], ["后排颜色", rearColor]];
|
const rawPower = normalizeDeviceReadout(readText("#ambientPower"));
|
||||||
|
const power = rawPower === "-" ? "-" : /OFF|关闭/.test(rawPower) ? "关闭" : "开启";
|
||||||
|
return [["开关状态", power], ["前排亮度", frontBrightness], ["前排颜色", frontColor], ["后排亮度", rearBrightness], ["后排颜色", rearColor]];
|
||||||
},
|
},
|
||||||
controls: () => {
|
controls: () => {
|
||||||
const [frontBrightness, rearBrightness] = splitReadout("#ambientBrightness");
|
const [frontBrightness, rearBrightness] = splitReadout("#ambientBrightness");
|
||||||
@@ -954,8 +984,8 @@
|
|||||||
const grid = $("#v2DeviceSyncGrid");
|
const grid = $("#v2DeviceSyncGrid");
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
grid.innerHTML = syncedDevices.map((device) => {
|
grid.innerHTML = syncedDevices.map((device) => {
|
||||||
const rows = device.rows();
|
const rows = deviceReadoutRows(device);
|
||||||
const waiting = rows.some(([, value]) => /--|等待|同步中|未知/.test(value));
|
const waiting = rows.some(([, value]) => value === "-");
|
||||||
const controllable = device.controllable !== false;
|
const controllable = device.controllable !== false;
|
||||||
const controlOpen = controllable && displaySync.controlDevice === device.id;
|
const controlOpen = controllable && displaySync.controlDevice === device.id;
|
||||||
const hasDraft = Boolean(displaySync.deviceDrafts[device.id]);
|
const hasDraft = Boolean(displaySync.deviceDrafts[device.id]);
|
||||||
@@ -979,7 +1009,7 @@
|
|||||||
if (displaySync.activeDevice) {
|
if (displaySync.activeDevice) {
|
||||||
$(`.v2-synced-device[data-device="${displaySync.activeDevice}"]`)?.classList.add("active");
|
$(`.v2-synced-device[data-device="${displaySync.activeDevice}"]`)?.classList.add("active");
|
||||||
}
|
}
|
||||||
displaySync.deviceFingerprint = JSON.stringify(syncedDevices.map((device) => [device.id, device.rows()]));
|
displaySync.deviceFingerprint = JSON.stringify(syncedDevices.map((device) => [device.id, deviceReadoutRows(device)]));
|
||||||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1071,7 +1101,7 @@
|
|||||||
if (displaySync.deviceFrame) return;
|
if (displaySync.deviceFrame) return;
|
||||||
displaySync.deviceFrame = requestAnimationFrame(() => {
|
displaySync.deviceFrame = requestAnimationFrame(() => {
|
||||||
displaySync.deviceFrame = null;
|
displaySync.deviceFrame = null;
|
||||||
const fingerprint = JSON.stringify(syncedDevices.map((device) => [device.id, device.rows()]));
|
const fingerprint = JSON.stringify(syncedDevices.map((device) => [device.id, deviceReadoutRows(device)]));
|
||||||
if (fingerprint === displaySync.deviceFingerprint && $("#v2DeviceSyncGrid")?.childElementCount) return;
|
if (fingerprint === displaySync.deviceFingerprint && $("#v2DeviceSyncGrid")?.childElementCount) return;
|
||||||
renderSyncedDevices();
|
renderSyncedDevices();
|
||||||
if (displaySync.activeDevice) presentTwinDevice(displaySync.activeDevice, {
|
if (displaySync.activeDevice) presentTwinDevice(displaySync.activeDevice, {
|
||||||
@@ -1157,7 +1187,7 @@
|
|||||||
}
|
}
|
||||||
displaySync.activeDevice = deviceId;
|
displaySync.activeDevice = deviceId;
|
||||||
if (pinned) displaySync.pinnedDevice = deviceId;
|
if (pinned) displaySync.pinnedDevice = deviceId;
|
||||||
const rows = device.rows();
|
const rows = deviceReadoutRows(device);
|
||||||
popover.hidden = false;
|
popover.hidden = false;
|
||||||
popover.dataset.device = deviceId;
|
popover.dataset.device = deviceId;
|
||||||
popover.dataset.pinned = String(Boolean(displaySync.pinnedDevice));
|
popover.dataset.pinned = String(Boolean(displaySync.pinnedDevice));
|
||||||
@@ -1341,30 +1371,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyDisplaySnapshot(snapshot) {
|
function applyDisplaySnapshot(snapshot) {
|
||||||
const health = snapshot?.extension_health || {};
|
|
||||||
const physicallyConnected = health.transport !== "simulation"
|
|
||||||
&& Boolean(health.connected || health.peer_connected);
|
|
||||||
const synchronizedCatalog = Array.isArray(snapshot?.extension?.catalog)
|
const synchronizedCatalog = Array.isArray(snapshot?.extension?.catalog)
|
||||||
? snapshot.extension.catalog
|
? snapshot.extension.catalog
|
||||||
: [];
|
: [];
|
||||||
const connected = physicallyConnected || synchronizedCatalog.length > 0;
|
|
||||||
const listening = Boolean(health.listening);
|
|
||||||
const usbState = $("#v2UsbState");
|
|
||||||
if (usbState) {
|
|
||||||
const usbLabel = $("span", usbState);
|
|
||||||
if (usbLabel) usbLabel.textContent = physicallyConnected
|
|
||||||
? "USB 外设已接入"
|
|
||||||
: synchronizedCatalog.length
|
|
||||||
? `${new Set(synchronizedCatalog.map((item) => item.device_id)).size} 个外设已同步`
|
|
||||||
: listening ? "等待 USB 外设" : "扩展链路检查中";
|
|
||||||
usbState.dataset.state = connected ? "online" : listening ? "listening" : "offline";
|
|
||||||
}
|
|
||||||
const vehicleOnline = health.vehicle?.connected !== false;
|
|
||||||
const vehicleBadge = $("#v2HeadUnitState");
|
|
||||||
if (vehicleBadge) {
|
|
||||||
vehicleBadge.dataset.state = vehicleOnline ? "online" : "offline";
|
|
||||||
$("span", vehicleBadge).textContent = vehicleOnline ? "车机链路在线" : "车机链路离线";
|
|
||||||
}
|
|
||||||
const revision = Number(snapshot?.revision ?? -1);
|
const revision = Number(snapshot?.revision ?? -1);
|
||||||
if (revision !== displaySync.revision) {
|
if (revision !== displaySync.revision) {
|
||||||
displaySync.revision = revision;
|
displaySync.revision = revision;
|
||||||
@@ -1417,13 +1426,9 @@
|
|||||||
});
|
});
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
applyDisplaySnapshot(await response.json());
|
applyDisplaySnapshot(await response.json());
|
||||||
$("#v2MirrorState")?.setAttribute("data-state", "online");
|
|
||||||
if ($("#v2MirrorState span")) $("#v2MirrorState span").textContent = "同步正常";
|
|
||||||
appendSyncLog({ key: "sync:online", category: "sync", title: "网页同步链路已建立", detail: "车机任务与设备状态每 2 秒同步", status: "success" });
|
appendSyncLog({ key: "sync:online", category: "sync", title: "网页同步链路已建立", detail: "车机任务与设备状态每 2 秒同步", status: "success" });
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
console.warn("[vehicle-display-sync]", _error);
|
console.warn("[vehicle-display-sync]", _error);
|
||||||
$("#v2MirrorState")?.setAttribute("data-state", "offline");
|
|
||||||
if ($("#v2MirrorState span")) $("#v2MirrorState span").textContent = "等待同步服务";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1488,11 +1493,6 @@
|
|||||||
const syncSidebar = make("section", "v2-sync-sidebar", `
|
const syncSidebar = make("section", "v2-sync-sidebar", `
|
||||||
<div class="v2-sync-sidebar-overview">
|
<div class="v2-sync-sidebar-overview">
|
||||||
<div class="v2-sync-sidebar-heading"><h2>511工程-智能座舱</h2></div>
|
<div class="v2-sync-sidebar-heading"><h2>511工程-智能座舱</h2></div>
|
||||||
<div class="v2-sync-link-stack">
|
|
||||||
<div class="v2-sync-link" id="v2HeadUnitState" data-state="checking"><i data-lucide="monitor-smartphone"></i><div><strong>车机链路</strong><span>连接检查中</span></div><b></b></div>
|
|
||||||
<div class="v2-sync-link" id="v2MirrorState" data-state="checking"><i data-lucide="refresh-cw"></i><div><strong>网页同步</strong><span>建立同步中</span></div><b></b></div>
|
|
||||||
<div class="v2-sync-link" id="v2UsbState" data-state="checking"><i data-lucide="usb"></i><div><strong>USB 扩展</strong><span>检查外设中</span></div><b></b></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<section class="v2-sync-log" aria-labelledby="v2SyncLogTitle">
|
<section class="v2-sync-log" aria-labelledby="v2SyncLogTitle">
|
||||||
<header><div><h3 id="v2SyncLogTitle">运行日志 / 同步记录</h3></div><select id="v2LogFilter" aria-label="筛选同步日志"><option value="all">全部</option><option value="task">任务</option><option value="sync">同步</option><option value="extension">外设</option><option value="device">设备</option></select></header>
|
<header><div><h3 id="v2SyncLogTitle">运行日志 / 同步记录</h3></div><select id="v2LogFilter" aria-label="筛选同步日志"><option value="all">全部</option><option value="task">任务</option><option value="sync">同步</option><option value="extension">外设</option><option value="device">设备</option></select></header>
|
||||||
@@ -1505,12 +1505,10 @@
|
|||||||
const executionField = $(".field-group:has(.execution-mode-options)");
|
const executionField = $(".field-group:has(.execution-mode-options)");
|
||||||
const simulationFaultField = $("#simulationFaultField");
|
const simulationFaultField = $("#simulationFaultField");
|
||||||
if (syncOverview) {
|
if (syncOverview) {
|
||||||
const linkStack = $(".v2-sync-link-stack", syncOverview);
|
|
||||||
const insertVisibleControl = (field, className) => {
|
const insertVisibleControl = (field, className) => {
|
||||||
if (!field) return;
|
if (!field) return;
|
||||||
field.classList.add(className);
|
field.classList.add(className);
|
||||||
if (linkStack) syncOverview.insertBefore(field, linkStack);
|
syncOverview.append(field);
|
||||||
else syncOverview.append(field);
|
|
||||||
};
|
};
|
||||||
insertVisibleControl(transportField, "v2-sync-transport-field");
|
insertVisibleControl(transportField, "v2-sync-transport-field");
|
||||||
if (executionField) executionField.hidden = false;
|
if (executionField) executionField.hidden = false;
|
||||||
@@ -1526,8 +1524,7 @@
|
|||||||
<p>等待自然语言解析结果</p>
|
<p>等待自然语言解析结果</p>
|
||||||
</div>`);
|
</div>`);
|
||||||
canCommandPanel.id = "v2CanCommandPanel";
|
canCommandPanel.id = "v2CanCommandPanel";
|
||||||
if (linkStack) syncOverview.insertBefore(canCommandPanel, linkStack);
|
syncOverview.append(canCommandPanel);
|
||||||
else syncOverview.append(canCommandPanel);
|
|
||||||
canCommandPanel.addEventListener("toggle", () => {
|
canCommandPanel.addEventListener("toggle", () => {
|
||||||
requestAnimationFrame(syncSidebarLogBaseline);
|
requestAnimationFrame(syncSidebarLogBaseline);
|
||||||
});
|
});
|
||||||
@@ -1790,7 +1787,6 @@
|
|||||||
if (requestStatus && statusTarget && requestStatus.parentElement !== statusTarget) {
|
if (requestStatus && statusTarget && requestStatus.parentElement !== statusTarget) {
|
||||||
statusTarget.append(requestStatus);
|
statusTarget.append(requestStatus);
|
||||||
}
|
}
|
||||||
requestAnimationFrame(updateModeEdgeHints);
|
|
||||||
const drawer = $("#gestureDrawer");
|
const drawer = $("#gestureDrawer");
|
||||||
if (!triggerGesture) {
|
if (!triggerGesture) {
|
||||||
const gestureVisible = ui.controlMode === "gesture";
|
const gestureVisible = ui.controlMode === "gesture";
|
||||||
@@ -1902,32 +1898,19 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
window.addEventListener("resize", updateScrollCues);
|
window.addEventListener("resize", updateScrollCues);
|
||||||
window.addEventListener("resize", updateModeEdgeHints);
|
|
||||||
window.addEventListener("cognitive-agent:result", () => requestAnimationFrame(updateScrollCues));
|
window.addEventListener("cognitive-agent:result", () => requestAnimationFrame(updateScrollCues));
|
||||||
const modeChips = $(".v2-mode-rail .mode-chips");
|
|
||||||
modeChips?.addEventListener(
|
|
||||||
"scroll",
|
|
||||||
() => requestAnimationFrame(updateModeEdgeHints),
|
|
||||||
{ passive: true },
|
|
||||||
);
|
|
||||||
modeChips?.addEventListener("wheel", (event) => {
|
|
||||||
if (modeChips.scrollWidth <= modeChips.clientWidth + 2) return;
|
|
||||||
const distance = Math.abs(event.deltaX) > Math.abs(event.deltaY)
|
|
||||||
? event.deltaX
|
|
||||||
: event.deltaY;
|
|
||||||
if (!distance) return;
|
|
||||||
event.preventDefault();
|
|
||||||
modeChips.scrollBy({ left: distance, behavior: "smooth" });
|
|
||||||
window.requestAnimationFrame(updateModeEdgeHints);
|
|
||||||
}, { passive: false });
|
|
||||||
if (modeChips) {
|
|
||||||
new MutationObserver(() => requestAnimationFrame(updateModeEdgeHints))
|
|
||||||
.observe(modeChips, { childList: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("cognitive-agent:connection", (event) => {
|
window.addEventListener("cognitive-agent:connection", (event) => {
|
||||||
syncRuntimePresentation(event.detail);
|
syncRuntimePresentation(event.detail);
|
||||||
});
|
});
|
||||||
|
window.addEventListener("cognitive-agent:vehicle-feedback-state", () => {
|
||||||
|
renderSyncedDevices();
|
||||||
|
if (displaySync.activeDevice) {
|
||||||
|
presentTwinDevice(displaySync.activeDevice, {
|
||||||
|
pinned: Boolean(displaySync.pinnedDevice),
|
||||||
|
selectSeat: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
const observer = new MutationObserver(() => keepRunButtonAvailable());
|
const observer = new MutationObserver(() => keepRunButtonAvailable());
|
||||||
[$("#hardwareMode"), $("#simulationMode")]
|
[$("#hardwareMode"), $("#simulationMode")]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@@ -1982,7 +1965,6 @@
|
|||||||
syncRuntimePresentation();
|
syncRuntimePresentation();
|
||||||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
updateModeEdgeHints();
|
|
||||||
document.body.classList.add("v2-ready");
|
document.body.classList.add("v2-ready");
|
||||||
window.setTimeout(() => $("#v2BootScreen")?.remove(), 220);
|
window.setTimeout(() => $("#v2BootScreen")?.remove(), 220);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,39 +1,76 @@
|
|||||||
:root { color-scheme: dark; font-family: Inter, "Microsoft YaHei", system-ui, sans-serif; background: #070b12; color: #edf4ff; }
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0.008 326);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0.008 326);
|
||||||
|
--primary: oklch(0.488 0.243 264.376);
|
||||||
|
--primary-foreground: oklch(0.97 0.014 254.604);
|
||||||
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--muted: oklch(0.96 0.003 325.6);
|
||||||
|
--muted-foreground: oklch(0.542 0.034 322.5);
|
||||||
|
--accent: oklch(0.96 0.003 325.6);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0.005 325.62);
|
||||||
|
--ring: oklch(0.711 0.019 323.02);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
font-family: Inter, "Microsoft YaHei", system-ui, sans-serif;
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
.dark {
|
||||||
|
color-scheme: dark;
|
||||||
|
--background: oklch(0.145 0.008 326);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.212 0.019 322.12);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.424 0.199 265.638);
|
||||||
|
--primary-foreground: oklch(0.97 0.014 254.604);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.263 0.024 320.12);
|
||||||
|
--muted-foreground: oklch(0.711 0.019 323.02);
|
||||||
|
--accent: oklch(0.263 0.024 320.12);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--ring: oklch(0.542 0.034 322.5);
|
||||||
|
}
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
body { margin: 0; min-height: 100vh; background: radial-gradient(circle at 18% 0%, #17294b 0, transparent 35%), #070b12; }
|
body { margin: 0; min-height: 100vh; background: radial-gradient(circle at 18% 0%, color-mix(in oklch, var(--primary) 12%, transparent) 0, transparent 35%), var(--background); }
|
||||||
.shell { width: min(1040px, calc(100% - 40px)); margin: 0 auto; padding: 54px 0 80px; }
|
.shell { width: min(1040px, calc(100% - 40px)); margin: 0 auto; padding: 54px 0 80px; }
|
||||||
header, .section-title, .policy, .request, .lease-details { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
|
header, .section-title, .policy, .request, .lease-details { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
|
||||||
header { margin-bottom: 34px; }
|
header { margin-bottom: 34px; }
|
||||||
h1, h2, p { margin: 0; }
|
h1, h2, p { margin: 0; }
|
||||||
h1 { font-size: clamp(30px, 5vw, 48px); letter-spacing: -.04em; }
|
h1 { font-size: clamp(30px, 5vw, 48px); letter-spacing: -.04em; }
|
||||||
h2 { font-size: 22px; }
|
h2 { font-size: 22px; }
|
||||||
.eyebrow { color: #65a8ff; font-size: 12px; letter-spacing: .16em; font-weight: 800; margin-bottom: 8px; }
|
.eyebrow { color: var(--primary); font-size: 12px; letter-spacing: .16em; font-weight: 800; margin-bottom: 8px; }
|
||||||
.lead, small { color: #8d9caf; }
|
.lead, small { color: var(--muted-foreground); }
|
||||||
.lead { margin-top: 12px; }
|
.lead { margin-top: 12px; }
|
||||||
a, button { font: inherit; }
|
a, button { font: inherit; }
|
||||||
a { color: #cfe4ff; text-decoration: none; border: 1px solid #2b3a50; border-radius: 10px; padding: 10px 14px; }
|
a { color: var(--primary); text-decoration: none; border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 14px; }
|
||||||
.policy, .card { background: rgba(15, 23, 36, .88); border: 1px solid #26354a; border-radius: 18px; box-shadow: 0 20px 60px rgba(0,0,0,.22); }
|
.policy, .card { color: var(--card-foreground); background: color-mix(in oklch, var(--card) 94%, transparent); border: 1px solid var(--border); border-radius: calc(var(--radius) + 8px); box-shadow: 0 20px 60px oklch(0.145 0.008 326 / 12%); }
|
||||||
.policy { padding: 18px 22px; justify-content: flex-start; margin-bottom: 18px; }
|
.policy { padding: 18px 22px; justify-content: flex-start; margin-bottom: 18px; }
|
||||||
.policy div { display: grid; gap: 3px; flex: 1; }
|
.policy div { display: grid; gap: 3px; flex: 1; }
|
||||||
.dot { width: 10px; height: 10px; border-radius: 50%; background: #41d390; box-shadow: 0 0 16px #41d390; }
|
.dot { width: 10px; height: 10px; border-radius: 50%; background: var(--primary); box-shadow: 0 0 16px color-mix(in oklch, var(--primary) 55%, transparent); }
|
||||||
#clock { color: #8d9caf; font-variant-numeric: tabular-nums; }
|
#clock { color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
|
||||||
.card { padding: 24px; margin-top: 18px; }
|
.card { padding: 24px; margin-top: 18px; }
|
||||||
.active-card { border-color: #315a86; }
|
.active-card { border-color: var(--ring); }
|
||||||
.section-title { margin-bottom: 20px; }
|
.section-title { margin-bottom: 20px; }
|
||||||
.count { min-width: 30px; height: 30px; display: grid; place-items: center; border-radius: 999px; background: #1a2d48; color: #7fbbff; font-weight: 800; }
|
.count { min-width: 30px; height: 30px; display: grid; place-items: center; border-radius: 999px; background: var(--accent); color: var(--primary); font-weight: 800; }
|
||||||
.empty { padding: 26px 4px; color: #74849a; text-align: center; }
|
.empty { padding: 26px 4px; color: var(--muted-foreground); text-align: center; }
|
||||||
.request-list { display: grid; gap: 12px; }
|
.request-list { display: grid; gap: 12px; }
|
||||||
.request { padding: 16px; border-radius: 13px; border: 1px solid #26364d; background: #0b121e; }
|
.request { padding: 16px; border-radius: calc(var(--radius) + 3px); border: 1px solid var(--border); background: var(--muted); }
|
||||||
.identity { display: grid; gap: 5px; min-width: 0; }
|
.identity { display: grid; gap: 5px; min-width: 0; }
|
||||||
.identity strong { font-size: 17px; }
|
.identity strong { font-size: 17px; }
|
||||||
.identity code, .identity span { color: #8394aa; font-size: 12px; overflow-wrap: anywhere; }
|
.identity code, .identity span { color: var(--muted-foreground); font-size: 12px; overflow-wrap: anywhere; }
|
||||||
.actions { display: flex; gap: 8px; }
|
.actions { display: flex; gap: 8px; }
|
||||||
button { color: #f4f8ff; background: #1577db; border: 0; border-radius: 9px; padding: 10px 14px; cursor: pointer; font-weight: 750; }
|
button { color: var(--primary-foreground); background: var(--primary); border: 0; border-radius: var(--radius); padding: 10px 14px; cursor: pointer; font-weight: 750; }
|
||||||
button:hover { filter: brightness(1.12); }
|
button:hover { filter: brightness(1.12); }
|
||||||
button:disabled { opacity: .45; cursor: wait; }
|
button:disabled { opacity: .45; cursor: wait; }
|
||||||
button.secondary { background: #243348; }
|
button.secondary { color: var(--secondary-foreground); background: var(--secondary); }
|
||||||
button.danger { background: #b83449; }
|
button.danger { color: var(--primary-foreground); background: var(--destructive); }
|
||||||
.lease-details { justify-content: flex-start; flex-wrap: wrap; padding: 18px; border-radius: 13px; background: #0b121e; border: 1px solid #284f78; }
|
.lease-details { justify-content: flex-start; flex-wrap: wrap; padding: 18px; border-radius: calc(var(--radius) + 3px); background: var(--muted); border: 1px solid var(--ring); }
|
||||||
.lease-details .timer { margin-left: auto; color: #4ce0a0; font-weight: 850; font-size: 22px; font-variant-numeric: tabular-nums; }
|
.lease-details .timer { margin-left: auto; color: var(--primary); font-weight: 850; font-size: 22px; font-variant-numeric: tabular-nums; }
|
||||||
.message { margin-top: 18px; min-height: 24px; color: #7fbbff; }
|
.message { margin-top: 18px; min-height: 24px; color: var(--primary); }
|
||||||
@media (max-width: 680px) { .shell { width: min(100% - 24px, 1040px); padding-top: 26px; } header, .request, .lease-details { align-items: flex-start; flex-direction: column; } .actions { width: 100%; } .actions button { flex: 1; } .lease-details .timer { margin-left: 0; } }
|
@media (max-width: 680px) { .shell { width: min(100% - 24px, 1040px); padding-top: 26px; } header, .request, .lease-details { align-items: flex-start; flex-direction: column; } .actions { width: 100%; } .actions button { flex: 1; } .lease-details .timer { margin-left: 0; } }
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="color-scheme" content="light dark">
|
||||||
|
<meta name="theme-color" content="oklch(0.488 0.243 264.376)">
|
||||||
<title>真实台架本地决策台</title>
|
<title>真实台架本地决策台</title>
|
||||||
<link rel="stylesheet" href="/hardware-control.css">
|
<link rel="stylesheet" href="/hardware-control.css?v=shadcn-theme-1">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="shell">
|
<main class="shell">
|
||||||
|
|||||||
+28
-38
@@ -3,26 +3,26 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
<meta name="color-scheme" content="light" />
|
<meta name="color-scheme" content="light dark" />
|
||||||
<meta name="theme-color" content="#146b65" />
|
<meta name="theme-color" content="oklch(0.488 0.243 264.376)" />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<link rel="icon" href="/icons/app-icon-192.png" />
|
<link rel="icon" href="/icons/app-icon-192.png" />
|
||||||
<title>511工程-智能座舱</title>
|
<title>511工程-智能座舱</title>
|
||||||
<link rel="stylesheet" href="/styles.css?v=dual-seat-16" />
|
<link rel="stylesheet" href="/styles.css?v=dual-seat-21" />
|
||||||
<link rel="stylesheet" href="/console-v2.css?v=delivery-state-44" />
|
<link rel="stylesheet" href="/console-v2.css?v=delivery-state-50" />
|
||||||
<link rel="stylesheet" href="/self-extension.css?v=gscu-tcp-40" />
|
<link rel="stylesheet" href="/self-extension.css?v=gscu-tcp-42" />
|
||||||
<link rel="stylesheet" href="/speech-input.css?v=sensevoice-4" />
|
<link rel="stylesheet" href="/speech-input.css?v=sensevoice-5" />
|
||||||
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
|
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
|
||||||
<script src="/vendor/three/0.147.0/three.min.js" defer></script>
|
<script src="/vendor/three/0.147.0/three.min.js" defer></script>
|
||||||
<script src="/vendor/three/0.147.0/fflate.min.js" defer></script>
|
<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="/vendor/three/0.147.0/FBXLoader.js" defer></script>
|
||||||
<script src="/network.js?v=android-pad-2" defer></script>
|
<script src="/network.js?v=android-pad-2" defer></script>
|
||||||
<script src="/app.js?v=dual-seat-110" defer></script>
|
<script src="/app.js?v=dual-seat-114" defer></script>
|
||||||
<script src="/speech-input.js?v=sensevoice-6" defer></script>
|
<script src="/speech-input.js?v=sensevoice-6" defer></script>
|
||||||
<script type="module" src="/gesture-control.js?v=android-pad-9"></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="/pwa.js?v=android-pad-3" defer></script>
|
||||||
<script src="/console-v2.js?v=delivery-state-51" defer></script>
|
<script src="/console-v2.js?v=delivery-state-54" defer></script>
|
||||||
<script src="/self-extension.js?v=gscu-tcp-41" defer></script>
|
<script src="/self-extension.js?v=gscu-tcp-41" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="console-v2">
|
<body class="console-v2">
|
||||||
@@ -37,29 +37,22 @@
|
|||||||
<h2>说一句,就调好了</h2>
|
<h2>说一句,就调好了</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mode-rail" aria-label="快捷功能模式">
|
<div class="mode-rail" aria-label="功能模式">
|
||||||
<div class="mode-rail-heading">
|
<div class="mode-rail-bar">
|
||||||
<div><span>功能模式</span></div>
|
<span class="mode-rail-title">功能模式</span>
|
||||||
<button id="saveModeToggle" type="button"><i data-lucide="bookmark-plus"></i>根据指令创建</button>
|
<button class="mode-rail-toggle" id="modeRailToggle" type="button" aria-expanded="false" aria-controls="modeRailContent">
|
||||||
</div>
|
<i data-lucide="layout-grid" data-icon="inline-start"></i><span id="modeRailSelection">常用模式</span><i data-lucide="chevron-down" data-icon="inline-end"></i>
|
||||||
<div class="mode-chips" aria-label="快捷功能模式" aria-live="polite">
|
</button>
|
||||||
<button type="button" data-mode-profile-id="zero_gravity" data-mode-command="进入零重力模式"><i data-lucide="orbit"></i><span>零重力</span></button>
|
<div class="mode-rail-actions">
|
||||||
<button type="button" data-mode-profile-id="office" data-mode-command="进入办公模式"><i data-lucide="briefcase"></i><span>办公</span></button>
|
<button class="mode-gallery-toggle" id="modeGalleryToggle" type="button" aria-expanded="false" aria-controls="modeGallery"><i data-lucide="layout-grid" data-icon="inline-start"></i><span>自定义</span><strong id="modeGalleryCount">0</strong></button>
|
||||||
<button type="button" data-mode-profile-id="nap" data-mode-command="进入小憩模式"><i data-lucide="moon-star"></i><span>小憩</span></button>
|
<button class="mode-create-toggle" id="saveModeToggle" type="button"><i data-lucide="bookmark-plus" data-icon="inline-start"></i><span>根据指令创建</span></button>
|
||||||
</div>
|
|
||||||
<div class="mode-rail-controls" aria-label="功能模式浏览">
|
|
||||||
<div class="mode-rail-pager" role="group" aria-label="翻阅快捷功能模式">
|
|
||||||
<button class="mode-rail-page" id="modeRailPrev" type="button" aria-label="查看上一组功能模式" title="上一组" disabled><i data-lucide="chevron-left"></i></button>
|
|
||||||
<button class="mode-rail-page" id="modeRailNext" type="button" aria-label="查看下一组功能模式" title="下一组"><i data-lucide="chevron-right"></i></button>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="mode-gallery-toggle" id="modeGalleryToggle" type="button" aria-expanded="false" aria-controls="modeGallery"><i data-lucide="layout-grid"></i><span>全部模式</span><strong id="modeGalleryCount">0</strong></button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="save-mode-composer" id="saveModeComposer" hidden>
|
<div class="mode-rail-content" id="modeRailContent" role="region" aria-label="功能模式列表" hidden>
|
||||||
<div class="save-mode-source"><small>将根据当前指令创建</small><p id="saveModeSource">请先在指令框输入需要固化的功能组合</p></div>
|
<div class="mode-chips" aria-label="快捷功能模式" aria-live="polite">
|
||||||
<input id="saveModeName" maxlength="24" placeholder="输入模式名称,例如:通勤模式" aria-label="新功能模式名称" />
|
<button type="button" data-mode-profile-id="zero_gravity" data-mode-command="进入零重力模式"><i data-lucide="orbit"></i><span>零重力</span></button>
|
||||||
<div class="save-mode-actions">
|
<button type="button" data-mode-profile-id="office" data-mode-command="进入办公模式"><i data-lucide="briefcase"></i><span>办公</span></button>
|
||||||
<button id="saveModeCancel" type="button">取消</button>
|
<button type="button" data-mode-profile-id="nap" data-mode-command="进入小憩模式"><i data-lucide="moon-star"></i><span>小憩</span></button>
|
||||||
<button id="saveModeConfirm" type="button">创建并添加</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,13 +108,10 @@
|
|||||||
<span class="field-label">执行目标</span>
|
<span class="field-label">执行目标</span>
|
||||||
<div class="segmented execution-mode-options" role="group" aria-label="执行目标">
|
<div class="segmented execution-mode-options" role="group" aria-label="执行目标">
|
||||||
<button type="button" class="mode-option" data-execution-mode="dry_run">
|
<button type="button" class="mode-option" data-execution-mode="dry_run">
|
||||||
<i data-lucide="flask-conical"></i>虚拟 CAN
|
<i data-lucide="flask-conical"></i>模拟下发
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="mode-option active" id="hardwareMode" data-execution-mode="hardware">
|
<button type="button" class="mode-option active" id="hardwareMode" data-execution-mode="hardware">
|
||||||
<i data-lucide="radio-tower"></i>真实台架
|
<i data-lucide="radio-tower"></i>真实下发
|
||||||
</button>
|
|
||||||
<button type="button" class="mode-option" id="simulationMode" data-execution-mode="simulated_hardware" disabled>
|
|
||||||
<i data-lucide="bug-play"></i>模拟台架
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="provider-note" id="hardwareNote">正在检查 CAN 适配器与 IAR 固件配置</p>
|
<p class="provider-note" id="hardwareNote">正在检查 CAN 适配器与 IAR 固件配置</p>
|
||||||
@@ -554,9 +544,9 @@
|
|||||||
<section class="mode-gallery" id="modeGallery" role="dialog" aria-modal="true" aria-labelledby="modeGalleryTitle">
|
<section class="mode-gallery" id="modeGallery" role="dialog" aria-modal="true" aria-labelledby="modeGalleryTitle">
|
||||||
<header class="mode-gallery-header">
|
<header class="mode-gallery-header">
|
||||||
<div>
|
<div>
|
||||||
<h2 id="modeGalleryTitle">全部功能模式</h2>
|
<h2 id="modeGalleryTitle">自定义模式</h2>
|
||||||
</div>
|
</div>
|
||||||
<button class="mode-gallery-close" id="modeGalleryClose" type="button" aria-label="关闭全部功能模式"><i data-lucide="x"></i></button>
|
<button class="mode-gallery-close" id="modeGalleryClose" type="button" aria-label="关闭自定义模式"><i data-lucide="x"></i></button>
|
||||||
</header>
|
</header>
|
||||||
<div class="mode-gallery-summary"><span><i data-lucide="layers-3"></i>可用模式</span><strong id="modeGallerySummaryCount">0</strong></div>
|
<div class="mode-gallery-summary"><span><i data-lucide="layers-3"></i>可用模式</span><strong id="modeGallerySummaryCount">0</strong></div>
|
||||||
<div class="mode-gallery-groups" id="modeGalleryGroups" aria-live="polite"></div>
|
<div class="mode-gallery-groups" id="modeGalleryGroups" aria-live="polite"></div>
|
||||||
@@ -601,8 +591,8 @@
|
|||||||
<p class="mode-editor-status" id="modeEditorStatus">修改自然语言后需要重新生成指令,才能点击完成永久保存。</p>
|
<p class="mode-editor-status" id="modeEditorStatus">修改自然语言后需要重新生成指令,才能点击完成永久保存。</p>
|
||||||
<footer class="mode-editor-actions">
|
<footer class="mode-editor-actions">
|
||||||
<button id="modeEditorCancel" type="button">取消</button>
|
<button id="modeEditorCancel" type="button">取消</button>
|
||||||
<button class="mode-editor-execute" id="modeEditorExecute" type="button"><i data-lucide="wand-sparkles"></i><span>生成指令</span></button>
|
<button class="mode-editor-execute" id="modeEditorExecute" type="button"><i data-lucide="wand-sparkles" data-icon="inline-start"></i><span>生成指令</span></button>
|
||||||
<button class="mode-editor-complete" id="modeEditorComplete" type="button" disabled><i data-lucide="save"></i><span>完成</span></button>
|
<button class="mode-editor-complete" id="modeEditorComplete" type="button" disabled><i data-lucide="save" data-icon="inline-start"></i><span>完成</span></button>
|
||||||
</footer>
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+73
-29
@@ -1,10 +1,52 @@
|
|||||||
:root {
|
:root {
|
||||||
color: #17232b;
|
color-scheme: light;
|
||||||
background: #e8efed;
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0.008 326);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0.008 326);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0.008 326);
|
||||||
|
--primary: oklch(0.488 0.243 264.376);
|
||||||
|
--primary-foreground: oklch(0.97 0.014 254.604);
|
||||||
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--muted: oklch(0.96 0.003 325.6);
|
||||||
|
--muted-foreground: oklch(0.542 0.034 322.5);
|
||||||
|
--accent: oklch(0.96 0.003 325.6);
|
||||||
|
--accent-foreground: oklch(0.212 0.019 322.12);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0.005 325.62);
|
||||||
|
--input: oklch(0.922 0.005 325.62);
|
||||||
|
--ring: oklch(0.711 0.019 323.02);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
color: var(--foreground);
|
||||||
|
background: var(--background);
|
||||||
font-family: Inter, "Noto Sans SC", "Microsoft YaHei", system-ui, sans-serif;
|
font-family: Inter, "Noto Sans SC", "Microsoft YaHei", system-ui, sans-serif;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
color-scheme: dark;
|
||||||
|
--background: oklch(0.145 0.008 326);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.212 0.019 322.12);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.212 0.019 322.12);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.424 0.199 265.638);
|
||||||
|
--primary-foreground: oklch(0.97 0.014 254.604);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.263 0.024 320.12);
|
||||||
|
--muted-foreground: oklch(0.711 0.019 323.02);
|
||||||
|
--accent: oklch(0.263 0.024 320.12);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.542 0.034 322.5);
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@@ -14,9 +56,9 @@ body {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background:
|
background:
|
||||||
linear-gradient(rgba(217, 229, 225, 0.7) 1px, transparent 1px),
|
linear-gradient(color-mix(in oklch, var(--border) 70%, transparent) 1px, transparent 1px),
|
||||||
linear-gradient(90deg, rgba(217, 229, 225, 0.7) 1px, transparent 1px),
|
linear-gradient(90deg, color-mix(in oklch, var(--border) 70%, transparent) 1px, transparent 1px),
|
||||||
#eef3f1;
|
var(--muted);
|
||||||
background-size: 34px 34px;
|
background-size: 34px 34px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,10 +72,12 @@ body {
|
|||||||
.login-panel {
|
.login-panel {
|
||||||
width: min(420px, 100%);
|
width: min(420px, 100%);
|
||||||
padding: 28px;
|
padding: 28px;
|
||||||
background: #fff;
|
color: var(--card-foreground);
|
||||||
border: 1px solid #cbd7d3;
|
background: var(--card);
|
||||||
border-top: 4px solid #08766e;
|
border: 1px solid var(--border);
|
||||||
box-shadow: 0 18px 50px rgba(24, 42, 48, 0.12);
|
border-top: 4px solid var(--primary);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: 0 18px 50px oklch(0.145 0.008 326 / 12%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-brand {
|
.login-brand {
|
||||||
@@ -41,7 +85,7 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding-bottom: 24px;
|
padding-bottom: 24px;
|
||||||
border-bottom: 1px solid #d9e1de;
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-brand > span {
|
.login-brand > span {
|
||||||
@@ -49,8 +93,8 @@ body {
|
|||||||
height: 42px;
|
height: 42px;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
color: #fff;
|
color: var(--primary-foreground);
|
||||||
background: #08766e;
|
background: var(--primary);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
@@ -67,7 +111,7 @@ body {
|
|||||||
|
|
||||||
.login-brand small {
|
.login-brand small {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
color: #67747b;
|
color: var(--muted-foreground);
|
||||||
font: 8px "SFMono-Regular", Consolas, monospace;
|
font: 8px "SFMono-Regular", Consolas, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +121,7 @@ body {
|
|||||||
|
|
||||||
.login-heading p {
|
.login-heading p {
|
||||||
margin: 0 0 7px;
|
margin: 0 0 7px;
|
||||||
color: #08766e;
|
color: var(--primary);
|
||||||
font: 750 9px "SFMono-Regular", Consolas, monospace;
|
font: 750 9px "SFMono-Regular", Consolas, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +136,7 @@ form {
|
|||||||
|
|
||||||
label {
|
label {
|
||||||
margin: 0 0 7px;
|
margin: 0 0 7px;
|
||||||
color: #4f5f66;
|
color: var(--muted-foreground);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
@@ -102,32 +146,32 @@ input {
|
|||||||
height: 44px;
|
height: 44px;
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
color: #17232b;
|
color: var(--foreground);
|
||||||
background: #f7f9f8;
|
background: var(--background);
|
||||||
border: 1px solid #bbc8c4;
|
border: 1px solid var(--input);
|
||||||
border-radius: 4px;
|
border-radius: calc(var(--radius) - 2px);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:focus {
|
input:focus {
|
||||||
outline: 3px solid rgba(50, 105, 168, 0.16);
|
outline: 3px solid color-mix(in oklch, var(--ring) 24%, transparent);
|
||||||
border-color: #3269a8;
|
border-color: var(--ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-message {
|
.login-message {
|
||||||
min-height: 17px;
|
min-height: 17px;
|
||||||
margin: -4px 0 10px;
|
margin: -4px 0 10px;
|
||||||
color: #bd3f49;
|
color: var(--destructive);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
height: 44px;
|
height: 44px;
|
||||||
color: #fff;
|
color: var(--primary-foreground);
|
||||||
background: #08766e;
|
background: var(--primary);
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 4px;
|
border-radius: calc(var(--radius) - 2px);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 750;
|
font-weight: 750;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
@@ -135,7 +179,7 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
background: #075c57;
|
background: color-mix(in oklch, var(--primary) 86%, var(--foreground));
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
@@ -148,7 +192,7 @@ footer {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 26px;
|
margin-top: 26px;
|
||||||
color: #67747b;
|
color: var(--muted-foreground);
|
||||||
font: 8px "SFMono-Regular", Consolas, monospace;
|
font: 8px "SFMono-Regular", Consolas, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,8 +200,8 @@ footer i {
|
|||||||
width: 7px;
|
width: 7px;
|
||||||
height: 7px;
|
height: 7px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #08766e;
|
background: var(--primary);
|
||||||
box-shadow: 0 0 0 4px rgba(8, 118, 110, 0.12);
|
box-shadow: 0 0 0 4px color-mix(in oklch, var(--primary) 12%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 460px) {
|
@media (max-width: 460px) {
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="color-scheme" content="light" />
|
<meta name="color-scheme" content="light dark" />
|
||||||
|
<meta name="theme-color" content="oklch(0.488 0.243 264.376)" />
|
||||||
<title>登录 · Cognitive Agent</title>
|
<title>登录 · Cognitive Agent</title>
|
||||||
<link rel="stylesheet" href="/login.css" />
|
<link rel="stylesheet" href="/login.css?v=shadcn-theme-1" />
|
||||||
<script src="/network.js?v=android-pad-2" defer></script>
|
<script src="/network.js?v=android-pad-2" defer></script>
|
||||||
<script src="/login.js" defer></script>
|
<script src="/login.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
"scope": "/",
|
"scope": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"orientation": "any",
|
"orientation": "any",
|
||||||
"background_color": "#eef3f1",
|
"background_color": "oklch(1 0 0)",
|
||||||
"theme_color": "#146b65",
|
"theme_color": "oklch(0.488 0.243 264.376)",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/icons/app-icon-192.png",
|
"src": "/icons/app-icon-192.png",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/* Self-extension workspace ------------------------------------------------- */
|
/* Self-extension workspace ------------------------------------------------- */
|
||||||
|
|
||||||
[data-extension-type="light"] { --extension-card-color: #eab858; }
|
[data-extension-type="light"] { --extension-card-color: var(--chart-1); }
|
||||||
[data-extension-type="sound"] { --extension-card-color: #658bd5; }
|
[data-extension-type="sound"] { --extension-card-color: var(--chart-2); }
|
||||||
[data-extension-type="action"] { --extension-card-color: #db7468; }
|
[data-extension-type="action"] { --extension-card-color: var(--destructive); }
|
||||||
[data-extension-type="doll"] { --extension-card-color: #cb86a4; }
|
[data-extension-type="doll"] { --extension-card-color: var(--chart-3); }
|
||||||
[data-extension-type="mechanical"] { --extension-card-color: #758e94; }
|
[data-extension-type="mechanical"] { --extension-card-color: var(--chart-4); }
|
||||||
[data-extension-type="other"] { --extension-card-color: #579a88; }
|
[data-extension-type="other"] { --extension-card-color: var(--chart-5); }
|
||||||
|
|
||||||
.console-v2 .v2-extension-sidebar,
|
.console-v2 .v2-extension-sidebar,
|
||||||
.console-v2 .v2-extension-footer,
|
.console-v2 .v2-extension-footer,
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
z-index: 24;
|
z-index: 24;
|
||||||
display: block;
|
display: block;
|
||||||
transform: translate3d(var(--pet-x), var(--pet-y), 0);
|
transform: translate3d(var(--pet-x), var(--pet-y), 0);
|
||||||
filter: drop-shadow(0 12px 14px rgba(9, 66, 63, 0.18));
|
filter: drop-shadow(0 12px 14px color-mix(in oklch, var(--primary) 18%, transparent));
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
@@ -75,11 +75,11 @@
|
|||||||
max-width: calc(100vw - 24px);
|
max-width: calc(100vw - 24px);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
color: #285153;
|
color: var(--popover-foreground);
|
||||||
background: rgba(255, 255, 255, 0.94);
|
background: color-mix(in oklch, var(--popover) 94%, transparent);
|
||||||
border: 1px solid rgba(14, 119, 115, 0.18);
|
border: 1px solid color-mix(in oklch, var(--primary) 18%, transparent);
|
||||||
border-radius: 11px 11px 2px 11px;
|
border-radius: 11px 11px 2px 11px;
|
||||||
box-shadow: 0 9px 24px rgba(22, 72, 70, 0.12);
|
box-shadow: 0 9px 24px oklch(0.145 0.008 326 / 12%);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
@@ -3543,3 +3543,353 @@ form > .v2-extension-primary {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Final semantic theme layer for extension surfaces and nested dialogs --- */
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-sync-mount,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-gateway,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-synced-device,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-nfc-inline,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-empty {
|
||||||
|
color: var(--card-foreground);
|
||||||
|
background: color-mix(in oklch, var(--card) 86%, transparent);
|
||||||
|
border-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-sync-mount {
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 84% 8%, color-mix(in oklch, var(--primary) 10%, transparent), transparent 18rem),
|
||||||
|
var(--card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-sync-heading h3,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-gateway strong,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-synced-device header strong,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-nfc-inline strong,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-empty strong,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-linkage strong {
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-sync-heading small,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-gateway small,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-synced-device header small,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-synced-device header b,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-nfc-inline p,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-empty,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-linkage small {
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-gateway > span,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-synced-device header > span,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-nfc-inline-icon {
|
||||||
|
color: var(--primary);
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: color-mix(in oklch, var(--primary) 18%, var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-gateway > b,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-synced-device > div span,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-nfc-inline small,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-nfc-inline > div > div i,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-linkage > span,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-linkage > svg {
|
||||||
|
color: var(--primary);
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-linkage {
|
||||||
|
border-top-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-nfc-launch,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-nfc-inline > button {
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
background: var(--primary);
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-nfc-launch:hover,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-nfc-inline > button:hover {
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
background: color-mix(in oklch, var(--primary) 86%, var(--foreground));
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-dialog,
|
||||||
|
.v2-nfc-dialog {
|
||||||
|
color: var(--popover-foreground);
|
||||||
|
background: var(--popover);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: 0 30px 90px color-mix(in oklch, var(--foreground) 24%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-dialog::backdrop {
|
||||||
|
background: color-mix(in oklch, var(--foreground) 50%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-dialog > header,
|
||||||
|
.v2-extension-dialog form > header,
|
||||||
|
.v2-nfc-dialog > header {
|
||||||
|
color: var(--foreground);
|
||||||
|
background: color-mix(in oklch, var(--popover) 94%, transparent);
|
||||||
|
border-bottom-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-dialog header p,
|
||||||
|
.v2-extension-dialog form > label,
|
||||||
|
.v2-extension-reset-dialog > label,
|
||||||
|
.v2-extension-type-chooser small,
|
||||||
|
.v2-extension-preset-preview > span,
|
||||||
|
.v2-extension-learn-steps,
|
||||||
|
.v2-extension-learn-fields label,
|
||||||
|
.v2-extension-learning-page p,
|
||||||
|
.v2-extension-test-page p,
|
||||||
|
.v2-gscu-status small,
|
||||||
|
.v2-gscu-status > b,
|
||||||
|
.v2-gscu-config-form > label,
|
||||||
|
.v2-gscu-actions label,
|
||||||
|
.v2-gscu-config-form label small,
|
||||||
|
.v2-gscu-actions > header small,
|
||||||
|
.v2-gscu-command-catalog > header small,
|
||||||
|
#extensionGscuNamedCommands article > header span,
|
||||||
|
#extensionGscuNamedCommands article label span,
|
||||||
|
.v2-nfc-waiting p,
|
||||||
|
.v2-nfc-detected-heading p,
|
||||||
|
.v2-nfc-recognized-heading p,
|
||||||
|
.v2-nfc-create-grid label > span,
|
||||||
|
.v2-nfc-existing > span,
|
||||||
|
.v2-nfc-capability-primary small,
|
||||||
|
.v2-nfc-recognized-actions > span,
|
||||||
|
.v2-nfc-footnote {
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-dialog header > button,
|
||||||
|
.v2-extension-dialog input,
|
||||||
|
.v2-extension-dialog select,
|
||||||
|
.v2-extension-dialog textarea,
|
||||||
|
.v2-extension-type-chooser > button,
|
||||||
|
.v2-extension-dialog-actions button,
|
||||||
|
.v2-gscu-action-grid button,
|
||||||
|
.v2-gscu-advanced button,
|
||||||
|
#extensionGscuNamedCommands button,
|
||||||
|
#extensionGscuNamedCommands input,
|
||||||
|
.v2-nfc-create-grid input,
|
||||||
|
.v2-nfc-create-grid select,
|
||||||
|
.v2-nfc-existing select {
|
||||||
|
color: var(--foreground);
|
||||||
|
background: var(--background);
|
||||||
|
border-color: var(--input);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-dialog header > button:hover,
|
||||||
|
.v2-extension-dialog header > button:focus-visible,
|
||||||
|
.v2-gscu-action-grid button:hover,
|
||||||
|
.v2-gscu-advanced button:hover,
|
||||||
|
#extensionGscuNamedCommands button:hover {
|
||||||
|
color: var(--accent-foreground);
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-dialog input:focus,
|
||||||
|
.v2-extension-dialog select:focus,
|
||||||
|
.v2-extension-dialog textarea:focus,
|
||||||
|
#extensionGscuNamedCommands input:focus,
|
||||||
|
.v2-nfc-create-grid input:focus,
|
||||||
|
.v2-nfc-create-grid select:focus,
|
||||||
|
.v2-nfc-existing select:focus {
|
||||||
|
border-color: var(--ring);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring) 20%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-primary,
|
||||||
|
.v2-extension-dialog-actions .v2-extension-primary,
|
||||||
|
.v2-nfc-primary-actions button,
|
||||||
|
.v2-nfc-recognized-actions button:not(.v2-extension-danger) {
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
background: var(--primary);
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-danger,
|
||||||
|
.v2-gscu-advanced button.danger,
|
||||||
|
#extensionGscuNamedCommands button.danger {
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
background: var(--destructive);
|
||||||
|
border-color: var(--destructive);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-type-chooser > button.active {
|
||||||
|
color: var(--primary);
|
||||||
|
background: color-mix(in oklch, var(--primary) 9%, var(--card));
|
||||||
|
border-color: color-mix(in oklch, var(--primary) 30%, var(--border));
|
||||||
|
box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--primary) 18%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-type-chooser > button > span,
|
||||||
|
.v2-extension-learn-target > span,
|
||||||
|
.v2-extension-learn-animation,
|
||||||
|
.v2-extension-preset-preview i,
|
||||||
|
.v2-extension-learn-steps b,
|
||||||
|
.v2-nfc-reader-visual,
|
||||||
|
.v2-nfc-detected-heading > span,
|
||||||
|
.v2-nfc-recognized-heading > span {
|
||||||
|
color: var(--primary);
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: color-mix(in oklch, var(--primary) 20%, var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-preset-preview,
|
||||||
|
.v2-extension-learn-target,
|
||||||
|
.v2-gscu-status,
|
||||||
|
.v2-gscu-actions,
|
||||||
|
.v2-gscu-device-group,
|
||||||
|
.v2-gscu-device-command-grid > article,
|
||||||
|
.v2-gscu-result,
|
||||||
|
#extensionGscuNamedCommands > p,
|
||||||
|
.v2-nfc-stage,
|
||||||
|
.v2-nfc-detected-heading,
|
||||||
|
.v2-nfc-recognized-heading,
|
||||||
|
.v2-nfc-capability-card {
|
||||||
|
color: var(--card-foreground);
|
||||||
|
background: var(--card);
|
||||||
|
border-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-learn-steps > i,
|
||||||
|
.v2-gscu-advanced,
|
||||||
|
.v2-gscu-command-catalog,
|
||||||
|
.v2-gscu-result[open] summary,
|
||||||
|
.v2-nfc-relearn {
|
||||||
|
border-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-learn-steps span.active,
|
||||||
|
.v2-extension-learn-target small,
|
||||||
|
.v2-extension-learning-page > strong,
|
||||||
|
.v2-gscu-status[data-state="online"] > svg,
|
||||||
|
.v2-gscu-status[data-state="online"] > b,
|
||||||
|
.v2-gscu-status[data-state="listening"] > svg,
|
||||||
|
.v2-gscu-status[data-state="listening"] > b,
|
||||||
|
.v2-gscu-device-group > header small,
|
||||||
|
.v2-gscu-device-group > header > b,
|
||||||
|
#extensionGscuNamedCommands b,
|
||||||
|
.v2-nfc-waiting small,
|
||||||
|
.v2-nfc-detected-heading small,
|
||||||
|
.v2-nfc-recognized-heading small,
|
||||||
|
.v2-nfc-capability-heading small,
|
||||||
|
.v2-nfc-capability-primary svg,
|
||||||
|
.v2-nfc-footnote svg {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-learn-steps span.active b,
|
||||||
|
.v2-extension-learn-animation > span,
|
||||||
|
.v2-extension-test-page > span {
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
background: var(--primary);
|
||||||
|
box-shadow: 0 0 0 12px color-mix(in oklch, var(--primary) 10%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-gscu-status[data-state="online"] {
|
||||||
|
background: color-mix(in oklch, var(--primary) 8%, var(--card));
|
||||||
|
border-color: color-mix(in oklch, var(--primary) 24%, var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-gscu-status[data-state="listening"] {
|
||||||
|
background: color-mix(in oklch, var(--chart-2) 8%, var(--card));
|
||||||
|
border-color: color-mix(in oklch, var(--chart-2) 24%, var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-gscu-device-group > header strong,
|
||||||
|
.v2-gscu-device-group > header span,
|
||||||
|
.v2-gscu-result summary,
|
||||||
|
.v2-nfc-waiting strong,
|
||||||
|
.v2-nfc-detected-heading strong,
|
||||||
|
.v2-nfc-recognized-heading strong,
|
||||||
|
.v2-nfc-capability-primary span {
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
#extensionGscuTcpResult {
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
background: color-mix(in oklch, var(--foreground) 90%, var(--background));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-nfc-stage > header {
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
background: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-nfc-stage > header > span,
|
||||||
|
.v2-nfc-stage > header small,
|
||||||
|
.v2-nfc-stage > header h3,
|
||||||
|
.v2-nfc-stage > header p,
|
||||||
|
.v2-nfc-stage > header > b,
|
||||||
|
.v2-nfc-stage > header > b[data-state="online"] {
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-nfc-stage > header > span,
|
||||||
|
.v2-nfc-stage > header > b,
|
||||||
|
.v2-nfc-stage > header > b[data-state="online"] {
|
||||||
|
background: color-mix(in oklch, var(--primary-foreground) 12%, transparent);
|
||||||
|
border-color: color-mix(in oklch, var(--primary-foreground) 20%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-nfc-inline[data-state="recognized"],
|
||||||
|
.v2-nfc-capability-card.learned {
|
||||||
|
background: color-mix(in oklch, var(--chart-1) 12%, var(--card));
|
||||||
|
border-color: color-mix(in oklch, var(--chart-3) 26%, var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-nfc-inline[data-state="new-card"],
|
||||||
|
.v2-nfc-capability-card.pending {
|
||||||
|
background: color-mix(in oklch, var(--chart-2) 10%, var(--card));
|
||||||
|
border-color: color-mix(in oklch, var(--chart-2) 24%, var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-nfc-relearn {
|
||||||
|
color: var(--primary);
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-nfc-relearn:hover {
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
background: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-toast {
|
||||||
|
color: var(--popover-foreground);
|
||||||
|
background: var(--popover);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: 0 14px 36px color-mix(in oklch, var(--foreground) 18%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-toast.success {
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
background: var(--primary);
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-toast.error {
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
background: var(--destructive);
|
||||||
|
border-color: var(--destructive);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-gateway > button {
|
||||||
|
color: var(--primary);
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: color-mix(in oklch, var(--primary) 22%, var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-gateway > button:hover,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-gateway > button:focus-visible {
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
background: var(--primary);
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
const CACHE_VERSION = "cognitive-agent-pad-v221";
|
const CACHE_VERSION = "cognitive-agent-pad-v227";
|
||||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||||
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
||||||
const APP_SHELL = [
|
const APP_SHELL = [
|
||||||
"/",
|
"/",
|
||||||
"/index.html",
|
"/index.html",
|
||||||
"/styles.css?v=dual-seat-16",
|
"/styles.css?v=dual-seat-21",
|
||||||
"/network.js?v=android-pad-2",
|
"/network.js?v=android-pad-2",
|
||||||
"/app.js?v=dual-seat-109",
|
"/app.js?v=dual-seat-114",
|
||||||
"/gesture-control.js?v=android-pad-9",
|
"/gesture-control.js?v=android-pad-9",
|
||||||
"/pwa.js?v=android-pad-3",
|
"/pwa.js?v=android-pad-3",
|
||||||
"/console-v2.css?v=delivery-state-43",
|
"/console-v2.css?v=delivery-state-50",
|
||||||
"/console-v2.js?v=delivery-state-50",
|
"/console-v2.js?v=delivery-state-54",
|
||||||
"/self-extension.css?v=gscu-tcp-39",
|
"/self-extension.css?v=gscu-tcp-42",
|
||||||
"/self-extension.js?v=gscu-tcp-40",
|
"/self-extension.js?v=gscu-tcp-41",
|
||||||
"/speech-input.css?v=sensevoice-4",
|
"/speech-input.css?v=sensevoice-5",
|
||||||
"/speech-input.js?v=sensevoice-6",
|
"/speech-input.js?v=sensevoice-6",
|
||||||
"/speech-audio-worklet.js?v=sensevoice-4",
|
"/speech-audio-worklet.js?v=sensevoice-4",
|
||||||
"/assets/pets/self-extension-cat.png?v=3",
|
"/assets/pets/self-extension-cat.png?v=3",
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
place-items: center;
|
place-items: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: #284647;
|
color: var(--foreground);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -35,19 +35,19 @@
|
|||||||
|
|
||||||
.console-v2 .v2-request-form .speech-dictation:hover:not(:disabled),
|
.console-v2 .v2-request-form .speech-dictation:hover:not(:disabled),
|
||||||
.console-v2 .v2-request-form .speech-dictation:focus-visible {
|
.console-v2 .v2-request-form .speech-dictation:focus-visible {
|
||||||
color: #08766e;
|
color: var(--primary);
|
||||||
background: #eaf5f3;
|
background: color-mix(in oklch, var(--primary) 9%, var(--background));
|
||||||
box-shadow: 0 0 0 3px rgba(8, 118, 110, 0.08);
|
box-shadow: 0 0 0 3px color-mix(in oklch, var(--primary) 8%, transparent);
|
||||||
transform: translateY(-50%) scale(1.05);
|
transform: translateY(-50%) scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-v2 .v2-request-form .speech-dictation:focus-visible {
|
.console-v2 .v2-request-form .speech-dictation:focus-visible {
|
||||||
outline: 2px solid rgba(8, 118, 110, 0.48);
|
outline: 2px solid var(--ring);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-v2 .v2-request-form .speech-dictation:disabled {
|
.console-v2 .v2-request-form .speech-dictation:disabled {
|
||||||
color: #9aacaa;
|
color: var(--muted-foreground);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.56;
|
opacity: 0.56;
|
||||||
}
|
}
|
||||||
@@ -86,9 +86,9 @@
|
|||||||
.console-v2 .v2-request-form .speech-dictation.is-listening,
|
.console-v2 .v2-request-form .speech-dictation.is-listening,
|
||||||
.console-v2 .v2-request-form .speech-dictation.is-listening:hover:not(:disabled),
|
.console-v2 .v2-request-form .speech-dictation.is-listening:hover:not(:disabled),
|
||||||
.console-v2 .v2-request-form .speech-dictation.is-listening:focus-visible {
|
.console-v2 .v2-request-form .speech-dictation.is-listening:focus-visible {
|
||||||
color: #fff;
|
color: var(--primary-foreground);
|
||||||
background: #132c2c;
|
background: var(--primary);
|
||||||
box-shadow: 0 0 0 5px rgba(19, 44, 44, 0.09), 0 7px 18px rgba(17, 47, 46, 0.2);
|
box-shadow: 0 0 0 5px color-mix(in oklch, var(--primary) 9%, transparent), 0 7px 18px color-mix(in oklch, var(--primary) 20%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-v2 .v2-request-form .speech-dictation.is-listening .speech-mic-icon,
|
.console-v2 .v2-request-form .speech-dictation.is-listening .speech-mic-icon,
|
||||||
@@ -118,8 +118,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.console-v2 .v2-request-form .speech-dictation.is-processing {
|
.console-v2 .v2-request-form .speech-dictation.is-processing {
|
||||||
color: #08766e;
|
color: var(--primary);
|
||||||
background: #e5f3f1;
|
background: color-mix(in oklch, var(--primary) 10%, var(--background));
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-v2 .v2-request-form .speech-dictation.is-processing .speech-waveform {
|
.console-v2 .v2-request-form .speech-dictation.is-processing .speech-waveform {
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
left: 194px;
|
left: 194px;
|
||||||
z-index: 12;
|
z-index: 12;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: #5f7474;
|
color: var(--muted-foreground);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
@@ -145,29 +145,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.console-v2 .v2-request-form .command-composer[data-speech-state="listening"] {
|
.console-v2 .v2-request-form .command-composer[data-speech-state="listening"] {
|
||||||
border-color: rgba(19, 76, 73, 0.36);
|
border-color: color-mix(in oklch, var(--primary) 36%, transparent);
|
||||||
box-shadow: 0 12px 35px rgba(25, 72, 70, 0.1), inset 0 0 0 1px rgba(19, 76, 73, 0.08);
|
box-shadow: 0 12px 35px color-mix(in oklch, var(--primary) 10%, transparent), inset 0 0 0 1px color-mix(in oklch, var(--primary) 8%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-v2 .v2-request-form .command-composer[data-speech-state="listening"] .speech-status {
|
.console-v2 .v2-request-form .command-composer[data-speech-state="listening"] .speech-status {
|
||||||
color: #183e3d;
|
color: var(--foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-v2 .v2-request-form .command-composer[data-speech-state="processing"] .speech-status {
|
.console-v2 .v2-request-form .command-composer[data-speech-state="processing"] .speech-status {
|
||||||
color: #39706d;
|
color: color-mix(in oklch, var(--primary) 72%, var(--foreground));
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-v2 .v2-request-form .command-composer[data-speech-state="success"] .speech-status {
|
.console-v2 .v2-request-form .command-composer[data-speech-state="success"] .speech-status {
|
||||||
color: #08766e;
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-v2 .v2-request-form #utterance[data-speech-uncertain="true"] {
|
.console-v2 .v2-request-form #utterance[data-speech-uncertain="true"] {
|
||||||
background: linear-gradient(90deg, rgba(201, 132, 31, 0.055), transparent 42%);
|
background: linear-gradient(90deg, color-mix(in oklch, var(--chart-1) 10%, transparent), transparent 42%);
|
||||||
box-shadow: inset 3px 0 0 rgba(201, 132, 31, 0.58);
|
box-shadow: inset 3px 0 0 color-mix(in oklch, var(--chart-1) 58%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-v2 .v2-request-form .command-composer[data-speech-state="error"] .speech-status {
|
.console-v2 .v2-request-form .command-composer[data-speech-state="error"] .speech-status {
|
||||||
color: #b54b42;
|
color: var(--destructive);
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-v2 .v2-request-form .command-composer:not([data-speech-state="idle"]) .composer-hint {
|
.console-v2 .v2-request-form .command-composer:not([data-speech-state="idle"]) .composer-hint {
|
||||||
@@ -188,15 +188,15 @@
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
right: 98px;
|
right: 98px;
|
||||||
bottom: auto;
|
bottom: auto;
|
||||||
color: #75908e;
|
color: var(--muted-foreground);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-v2 .v2-request-form .context-reset:hover,
|
.console-v2 .v2-request-form .context-reset:hover,
|
||||||
.console-v2 .v2-request-form .context-reset:focus-visible {
|
.console-v2 .v2-request-form .context-reset:focus-visible {
|
||||||
color: #08766e;
|
color: var(--primary);
|
||||||
background: #edf6f4;
|
background: color-mix(in oklch, var(--primary) 8%, var(--background));
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-v2 .v2-request-form .composer-hint,
|
.console-v2 .v2-request-form .composer-hint,
|
||||||
|
|||||||
+613
-81
File diff suppressed because it is too large
Load Diff
@@ -671,8 +671,43 @@ class SeatDemoService:
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Persist one edited mode from a server-generated control flow."""
|
"""Persist one edited mode from a server-generated control flow."""
|
||||||
|
|
||||||
|
return self._persist_mode_profile_card(
|
||||||
|
mode_id,
|
||||||
|
name,
|
||||||
|
instruction,
|
||||||
|
source_request_id,
|
||||||
|
vehicle_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_mode_profile_card(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
instruction: str,
|
||||||
|
source_request_id: str,
|
||||||
|
vehicle_id: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Create one user mode from a server-generated control flow."""
|
||||||
|
|
||||||
|
return self._persist_mode_profile_card(
|
||||||
|
None,
|
||||||
|
name,
|
||||||
|
instruction,
|
||||||
|
source_request_id,
|
||||||
|
vehicle_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _persist_mode_profile_card(
|
||||||
|
self,
|
||||||
|
mode_id: str | None,
|
||||||
|
name: str,
|
||||||
|
instruction: str,
|
||||||
|
source_request_id: str,
|
||||||
|
vehicle_id: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Validate and persist a created or edited mode command flow."""
|
||||||
|
|
||||||
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
|
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
|
||||||
clean_mode_id = str(mode_id).strip()
|
clean_mode_id = str(mode_id or "").strip()
|
||||||
clean_name = str(name).strip()
|
clean_name = str(name).strip()
|
||||||
clean_instruction = str(instruction).strip()
|
clean_instruction = str(instruction).strip()
|
||||||
clean_request_id = str(source_request_id).strip()
|
clean_request_id = str(source_request_id).strip()
|
||||||
@@ -690,15 +725,19 @@ class SeatDemoService:
|
|||||||
visible_profiles = self.mode_profiles._profiles_for_vehicle( # noqa: SLF001
|
visible_profiles = self.mode_profiles._profiles_for_vehicle( # noqa: SLF001
|
||||||
resolved_vehicle_id
|
resolved_vehicle_id
|
||||||
)
|
)
|
||||||
profile = next(
|
profile = (
|
||||||
(item for item in visible_profiles if item.id == clean_mode_id),
|
next(
|
||||||
None,
|
(item for item in visible_profiles if item.id == clean_mode_id),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if clean_mode_id
|
||||||
|
else None
|
||||||
)
|
)
|
||||||
if profile is None:
|
if clean_mode_id and profile is None:
|
||||||
raise KeyError(clean_mode_id)
|
raise KeyError(clean_mode_id)
|
||||||
normalized_name = re.sub(r"\s+", "", clean_name).casefold()
|
normalized_name = re.sub(r"\s+", "", clean_name).casefold()
|
||||||
if any(
|
if any(
|
||||||
item.id != profile.id
|
(profile is None or item.id != profile.id)
|
||||||
and re.sub(r"\s+", "", item.name).casefold() == normalized_name
|
and re.sub(r"\s+", "", item.name).casefold() == normalized_name
|
||||||
for item in visible_profiles
|
for item in visible_profiles
|
||||||
):
|
):
|
||||||
@@ -813,14 +852,18 @@ class SeatDemoService:
|
|||||||
saved = self.mode_profiles.save_mode(
|
saved = self.mode_profiles.save_mode(
|
||||||
clean_name,
|
clean_name,
|
||||||
operations,
|
operations,
|
||||||
mode_id=profile.id,
|
mode_id=profile.id if profile is not None else None,
|
||||||
aliases=list(profile.aliases),
|
aliases=list(profile.aliases) if profile is not None else [],
|
||||||
vehicle_id=resolved_vehicle_id,
|
vehicle_id=resolved_vehicle_id,
|
||||||
description=profile.description,
|
description=(
|
||||||
icon=profile.icon,
|
profile.description
|
||||||
|
if profile is not None
|
||||||
|
else "由自然语言生成的自定义模式。"
|
||||||
|
),
|
||||||
|
icon=profile.icon if profile is not None else "bookmark-plus",
|
||||||
instruction=clean_instruction,
|
instruction=clean_instruction,
|
||||||
command_flow=command_flow,
|
command_flow=command_flow,
|
||||||
timeline=profile.timeline,
|
timeline=profile.timeline if profile is not None else None,
|
||||||
)
|
)
|
||||||
public_profile = next(
|
public_profile = next(
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
if path.startswith("/api/self-repair/"):
|
if path.startswith("/api/self-repair/"):
|
||||||
self._handle_self_repair_post(path)
|
self._handle_self_repair_post(path)
|
||||||
return
|
return
|
||||||
if path.startswith("/api/mode-profiles/"):
|
if path == "/api/mode-profiles" or path.startswith("/api/mode-profiles/"):
|
||||||
self._handle_mode_profile_post(path)
|
self._handle_mode_profile_post(path)
|
||||||
return
|
return
|
||||||
supplement_match = re.fullmatch(
|
supplement_match = re.fullmatch(
|
||||||
@@ -1506,8 +1506,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
self._handle_mode_profile_execute(execute_match.group("mode_id"))
|
self._handle_mode_profile_execute(execute_match.group("mode_id"))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
create_mode = path == "/api/mode-profiles"
|
||||||
mode_id = path.removeprefix("/api/mode-profiles/").strip()
|
mode_id = path.removeprefix("/api/mode-profiles/").strip()
|
||||||
if not mode_id:
|
if not create_mode and not mode_id:
|
||||||
self._json(
|
self._json(
|
||||||
{"error": "invalid_request", "message": "模式编号不能为空。"},
|
{"error": "invalid_request", "message": "模式编号不能为空。"},
|
||||||
HTTPStatus.BAD_REQUEST,
|
HTTPStatus.BAD_REQUEST,
|
||||||
@@ -1515,14 +1516,18 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
payload = self._read_json_body(max_bytes=16 * 1024)
|
payload = self._read_json_body(max_bytes=16 * 1024)
|
||||||
result = self.service.save_mode_profile_card(
|
arguments = (
|
||||||
mode_id,
|
|
||||||
str(payload.get("name", "")),
|
str(payload.get("name", "")),
|
||||||
str(payload.get("instruction", "")),
|
str(payload.get("instruction", "")),
|
||||||
str(payload.get("source_request_id", "")),
|
str(payload.get("source_request_id", "")),
|
||||||
str(payload.get("vehicle_id", "")) or None,
|
str(payload.get("vehicle_id", "")) or None,
|
||||||
)
|
)
|
||||||
self._json(result, HTTPStatus.OK)
|
result = (
|
||||||
|
self.service.create_mode_profile_card(*arguments)
|
||||||
|
if create_mode
|
||||||
|
else self.service.save_mode_profile_card(mode_id, *arguments)
|
||||||
|
)
|
||||||
|
self._json(result, HTTPStatus.CREATED if create_mode else HTTPStatus.OK)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self._json(
|
self._json(
|
||||||
{"error": "not_found", "message": "未找到需要编辑的模式卡片。"},
|
{"error": "not_found", "message": "未找到需要编辑的模式卡片。"},
|
||||||
|
|||||||
@@ -288,6 +288,35 @@ operations:
|
|||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
(user_root / "vehicles" / "denza_n7" / "user" / "office.yaml").is_file()
|
(user_root / "vehicles" / "denza_n7" / "user" / "office.yaml").is_file()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
create_request = urllib.request.Request(
|
||||||
|
f"{self.base_url}/api/mode-profiles",
|
||||||
|
method="POST",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
data=json.dumps(
|
||||||
|
{
|
||||||
|
"name": "通勤模式",
|
||||||
|
"instruction": instruction,
|
||||||
|
"source_request_id": request_id,
|
||||||
|
"vehicle_id": "denza_n7",
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
).encode("utf-8"),
|
||||||
|
)
|
||||||
|
with self.opener.open(create_request, timeout=3) as response:
|
||||||
|
created = json.loads(response.read().decode("utf-8"))
|
||||||
|
self.assertEqual(response.status, 201)
|
||||||
|
self.assertEqual(created["mode_profile"]["origin"], "user")
|
||||||
|
self.assertEqual(created["mode_profile"]["name"], "通勤模式")
|
||||||
|
self.assertTrue(
|
||||||
|
(
|
||||||
|
user_root
|
||||||
|
/ "vehicles"
|
||||||
|
/ "denza_n7"
|
||||||
|
/ "user"
|
||||||
|
/ f"{created['mode_profile']['id']}.yaml"
|
||||||
|
).is_file()
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
self.service.mode_profiles = original_registry
|
self.service.mode_profiles = original_registry
|
||||||
self.service.artifact_root = original_artifact_root
|
self.service.artifact_root = original_artifact_root
|
||||||
@@ -588,18 +617,18 @@ operations:
|
|||||||
def test_static_console_and_runtime_endpoint(self) -> None:
|
def test_static_console_and_runtime_endpoint(self) -> None:
|
||||||
with self.opener.open(f"{self.base_url}/", timeout=3) as response:
|
with self.opener.open(f"{self.base_url}/", timeout=3) as response:
|
||||||
html = response.read().decode("utf-8")
|
html = response.read().decode("utf-8")
|
||||||
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-109", timeout=3) as response:
|
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-114", timeout=3) as response:
|
||||||
app_script = response.read().decode("utf-8")
|
app_script = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
f"{self.base_url}/gesture-control.js?v=android-pad-9", timeout=3
|
f"{self.base_url}/gesture-control.js?v=android-pad-9", timeout=3
|
||||||
) as response:
|
) as response:
|
||||||
gesture_script = response.read().decode("utf-8")
|
gesture_script = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
f"{self.base_url}/console-v2.js?v=delivery-state-50", timeout=3
|
f"{self.base_url}/console-v2.js?v=delivery-state-54", timeout=3
|
||||||
) as response:
|
) as response:
|
||||||
console_script = response.read().decode("utf-8")
|
console_script = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
f"{self.base_url}/console-v2.css?v=delivery-state-43", timeout=3
|
f"{self.base_url}/console-v2.css?v=delivery-state-50", timeout=3
|
||||||
) as response:
|
) as response:
|
||||||
console_styles = response.read().decode("utf-8")
|
console_styles = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
@@ -607,7 +636,7 @@ operations:
|
|||||||
) as response:
|
) as response:
|
||||||
speech_script = response.read().decode("utf-8")
|
speech_script = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
f"{self.base_url}/speech-input.css?v=sensevoice-4", timeout=3
|
f"{self.base_url}/speech-input.css?v=sensevoice-5", timeout=3
|
||||||
) as response:
|
) as response:
|
||||||
speech_styles = response.read().decode("utf-8")
|
speech_styles = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
@@ -615,11 +644,11 @@ operations:
|
|||||||
) as response:
|
) as response:
|
||||||
speech_worklet = response.read().decode("utf-8")
|
speech_worklet = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
f"{self.base_url}/self-extension.js?v=gscu-tcp-40", timeout=3
|
f"{self.base_url}/self-extension.js?v=gscu-tcp-41", timeout=3
|
||||||
) as response:
|
) as response:
|
||||||
extension_script = response.read().decode("utf-8")
|
extension_script = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
f"{self.base_url}/self-extension.css?v=gscu-tcp-39", timeout=3
|
f"{self.base_url}/self-extension.css?v=gscu-tcp-42", timeout=3
|
||||||
) as response:
|
) as response:
|
||||||
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
|
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
@@ -652,21 +681,21 @@ operations:
|
|||||||
self.assertIn("CMD 0x431 · FB 0x41A · CH1", html)
|
self.assertIn("CMD 0x431 · FB 0x41A · CH1", html)
|
||||||
self.assertIn("CMD 0x431 · FB 0x388 · CH1", html)
|
self.assertIn("CMD 0x431 · FB 0x388 · CH1", html)
|
||||||
self.assertNotIn("透明车体 · 实体设备", html)
|
self.assertNotIn("透明车体 · 实体设备", html)
|
||||||
self.assertIn('src="/app.js?v=dual-seat-109"', html)
|
self.assertIn('src="/app.js?v=dual-seat-114"', html)
|
||||||
self.assertIn('href="/styles.css?v=dual-seat-16"', html)
|
self.assertIn('href="/styles.css?v=dual-seat-21"', html)
|
||||||
self.assertIn('src="/gesture-control.js?v=android-pad-9"', html)
|
self.assertIn('src="/gesture-control.js?v=android-pad-9"', html)
|
||||||
self.assertNotIn('class="vehicle-system-hud"', html)
|
self.assertNotIn('class="vehicle-system-hud"', html)
|
||||||
self.assertIn('src="/console-v2.js?v=delivery-state-50"', html)
|
self.assertIn('src="/console-v2.js?v=delivery-state-54"', html)
|
||||||
self.assertIn('href="/console-v2.css?v=delivery-state-43"', html)
|
self.assertIn('href="/console-v2.css?v=delivery-state-50"', html)
|
||||||
self.assertIn('src="/speech-input.js?v=sensevoice-6"', html)
|
self.assertIn('src="/speech-input.js?v=sensevoice-6"', html)
|
||||||
self.assertIn('href="/speech-input.css?v=sensevoice-4"', html)
|
self.assertIn('href="/speech-input.css?v=sensevoice-5"', html)
|
||||||
self.assertIn('id="speechDictationButton"', html)
|
self.assertIn('id="speechDictationButton"', html)
|
||||||
self.assertIn('class="speech-waveform"', html)
|
self.assertIn('class="speech-waveform"', html)
|
||||||
self.assertIn('<body class="console-v2">', html)
|
self.assertIn('<body class="console-v2">', html)
|
||||||
self.assertIn('id="v2BootScreen"', html)
|
self.assertIn('id="v2BootScreen"', html)
|
||||||
self.assertIn('正在加载车机控制与实时状态', html)
|
self.assertIn('正在加载车机控制与实时状态', html)
|
||||||
self.assertIn('src="/self-extension.js?v=gscu-tcp-40"', html)
|
self.assertIn('src="/self-extension.js?v=gscu-tcp-41"', html)
|
||||||
self.assertIn('href="/self-extension.css?v=gscu-tcp-39"', html)
|
self.assertIn('href="/self-extension.css?v=gscu-tcp-42"', html)
|
||||||
self.assertNotIn("self-calibration", html)
|
self.assertNotIn("self-calibration", html)
|
||||||
self.assertNotIn("self-repair", html)
|
self.assertNotIn("self-repair", html)
|
||||||
self.assertNotIn("self-evolution", html)
|
self.assertNotIn("self-evolution", html)
|
||||||
@@ -680,7 +709,7 @@ operations:
|
|||||||
'panelHeading.classList.add("v2-operation-heading", "v2-module-heading")',
|
'panelHeading.classList.add("v2-operation-heading", "v2-module-heading")',
|
||||||
console_script,
|
console_script,
|
||||||
)
|
)
|
||||||
self.assertIn('kicker.textContent = "REMOTE ORCHESTRATION · LIVE SYNC"', console_script)
|
self.assertIn('if (title) title.textContent = "一句话远程编排"', console_script)
|
||||||
self.assertIn('fetch("/api/vehicle-display-sync"', console_script)
|
self.assertIn('fetch("/api/vehicle-display-sync"', console_script)
|
||||||
self.assertIn('document.body.classList.add("v2-passive-display", "v2-interactive-sync")', console_script)
|
self.assertIn('document.body.classList.add("v2-passive-display", "v2-interactive-sync")', console_script)
|
||||||
self.assertIn('input.readOnly = false', console_script)
|
self.assertIn('input.readOnly = false', console_script)
|
||||||
@@ -713,7 +742,7 @@ operations:
|
|||||||
self.assertNotIn('id="v2VehicleState"', console_script)
|
self.assertNotIn('id="v2VehicleState"', console_script)
|
||||||
self.assertIn('actionRail.append(cameraToggle)', console_script)
|
self.assertIn('actionRail.append(cameraToggle)', console_script)
|
||||||
self.assertNotIn("function enhanceConfiguredDropdowns()", console_script)
|
self.assertNotIn("function enhanceConfiguredDropdowns()", console_script)
|
||||||
self.assertIn("function updateModeEdgeHints()", console_script)
|
self.assertNotIn('modeChips?.addEventListener("wheel"', console_script)
|
||||||
self.assertIn('classList.toggle("v2-twin-active", definition.index === 0)', console_script)
|
self.assertIn('classList.toggle("v2-twin-active", definition.index === 0)', console_script)
|
||||||
self.assertIn('label: "执行结果与硬件回读"', console_script)
|
self.assertIn('label: "执行结果与硬件回读"', console_script)
|
||||||
self.assertIn("function keepRunButtonAvailable()", console_script)
|
self.assertIn("function keepRunButtonAvailable()", console_script)
|
||||||
@@ -793,15 +822,33 @@ operations:
|
|||||||
)
|
)
|
||||||
self.assertIn('data-control-transport="wired_can"', html)
|
self.assertIn('data-control-transport="wired_can"', html)
|
||||||
self.assertIn('data-control-transport="wireless_mqtt"', html)
|
self.assertIn('data-control-transport="wireless_mqtt"', html)
|
||||||
|
self.assertIn('<div class="mode-rail" aria-label="功能模式">', html)
|
||||||
|
self.assertIn('id="modeRailToggle"', html)
|
||||||
|
self.assertIn('id="modeRailContent"', html)
|
||||||
|
self.assertIn('role="region"', html)
|
||||||
|
self.assertIn('aria-label="功能模式列表"', html)
|
||||||
|
self.assertIn('<span>自定义</span>', html)
|
||||||
|
self.assertIn('模拟下发', html)
|
||||||
|
self.assertIn('真实下发', html)
|
||||||
|
self.assertNotIn('id="simulationMode"', html)
|
||||||
self.assertIn('control_transport: state.controlTransport', app_script)
|
self.assertIn('control_transport: state.controlTransport', app_script)
|
||||||
|
self.assertIn('button.dataset.connectionState = connectionState', app_script)
|
||||||
|
self.assertIn('信号链路${connectionState === "online" ? "正常" : "断开"}', app_script)
|
||||||
self.assertIn('const transportField = $("#vehicleTransportField", originalForm)', console_script)
|
self.assertIn('const transportField = $("#vehicleTransportField", originalForm)', console_script)
|
||||||
self.assertNotIn('runtimeControls.append(transportField)', console_script)
|
self.assertNotIn('runtimeControls.append(transportField)', console_script)
|
||||||
self.assertIn('insertVisibleControl(transportField, "v2-sync-transport-field")', console_script)
|
self.assertIn('insertVisibleControl(transportField, "v2-sync-transport-field")', console_script)
|
||||||
|
self.assertNotIn('class="v2-sync-link-stack"', console_script)
|
||||||
|
self.assertNotIn('id="v2HeadUnitState"', console_script)
|
||||||
self.assertIn('.v2-sync-execution-field .execution-mode-options', console_styles)
|
self.assertIn('.v2-sync-execution-field .execution-mode-options', console_styles)
|
||||||
self.assertIn('grid-template-columns: repeat(3, minmax(0, 1fr))', console_styles)
|
self.assertIn(
|
||||||
|
'.v2-sync-execution-field .execution-mode-options {\n grid-template-columns: repeat(2, minmax(0, 1fr))',
|
||||||
|
console_styles,
|
||||||
|
)
|
||||||
self.assertNotIn('class="v2-sync-protocol"', console_script)
|
self.assertNotIn('class="v2-sync-protocol"', console_script)
|
||||||
self.assertIn('.console-v2 .v2-transport-field .vehicle-transport-options', console_styles)
|
self.assertIn('.console-v2 .v2-transport-field .vehicle-transport-options', console_styles)
|
||||||
self.assertIn('.v2-sync-transport-field .mode-option', console_styles)
|
self.assertIn('.v2-sync-transport-field .mode-option', console_styles)
|
||||||
|
self.assertIn('.v2-mode-rail > .mode-rail-content[hidden]', console_styles)
|
||||||
|
self.assertIn('font-size: var(--text-control) !important', console_styles)
|
||||||
self.assertIn('overflow-wrap: anywhere', console_styles)
|
self.assertIn('overflow-wrap: anywhere', console_styles)
|
||||||
self.assertIn('button.disabled = false', app_script)
|
self.assertIn('button.disabled = false', app_script)
|
||||||
self.assertIn('button.setAttribute("aria-pressed", String(selected))', app_script)
|
self.assertIn('button.setAttribute("aria-pressed", String(selected))', app_script)
|
||||||
@@ -846,7 +893,7 @@ operations:
|
|||||||
self.assertIn('外设玩具能力学习', extension_script)
|
self.assertIn('外设玩具能力学习', extension_script)
|
||||||
self.assertIn('外设玩具与车端联动', extension_script)
|
self.assertIn('外设玩具与车端联动', extension_script)
|
||||||
self.assertGreaterEqual(extension_script.count('class="v2-module-heading"'), 2)
|
self.assertGreaterEqual(extension_script.count('class="v2-module-heading"'), 2)
|
||||||
self.assertGreaterEqual(extension_script.count('SELF-EXTENSION'), 2)
|
self.assertNotIn('class="section-kicker"', extension_script)
|
||||||
self.assertIn('id="extensionExecuteButton"', extension_script)
|
self.assertIn('id="extensionExecuteButton"', extension_script)
|
||||||
self.assertIn('data-lucide="play"', extension_script)
|
self.assertIn('data-lucide="play"', extension_script)
|
||||||
self.assertNotIn('id="extensionControlHealth"', extension_script)
|
self.assertNotIn('id="extensionControlHealth"', extension_script)
|
||||||
@@ -912,10 +959,10 @@ operations:
|
|||||||
self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script)
|
self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script)
|
||||||
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
|
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
|
||||||
self.assertIn('"cognitive-agent:module-change"', app_script)
|
self.assertIn('"cognitive-agent:module-change"', app_script)
|
||||||
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v221"', service_worker)
|
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v227"', service_worker)
|
||||||
self.assertIn('"/app.js?v=dual-seat-109"', service_worker)
|
self.assertIn('"/app.js?v=dual-seat-114"', service_worker)
|
||||||
self.assertIn('"/speech-input.js?v=sensevoice-6"', service_worker)
|
self.assertIn('"/speech-input.js?v=sensevoice-6"', service_worker)
|
||||||
self.assertIn('"/speech-input.css?v=sensevoice-4"', service_worker)
|
self.assertIn('"/speech-input.css?v=sensevoice-5"', service_worker)
|
||||||
self.assertIn('"/speech-audio-worklet.js?v=sensevoice-4"', service_worker)
|
self.assertIn('"/speech-audio-worklet.js?v=sensevoice-4"', service_worker)
|
||||||
self.assertIn('fetch("/api/speech/transcribe"', speech_script)
|
self.assertIn('fetch("/api/speech/transcribe"', speech_script)
|
||||||
self.assertIn('"X-Speech-Utterance-Id"', speech_script)
|
self.assertIn('"X-Speech-Utterance-Id"', speech_script)
|
||||||
@@ -956,7 +1003,7 @@ operations:
|
|||||||
self.assertNotIn("gestureDebug", gesture_script)
|
self.assertNotIn("gestureDebug", gesture_script)
|
||||||
self.assertIn("HARDWARE_REQUEST_TIMEOUT_MS = 15 * 60 * 1000", app_script)
|
self.assertIn("HARDWARE_REQUEST_TIMEOUT_MS = 15 * 60 * 1000", app_script)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"requestTimeoutForExecutionMode(state.executionMode)", app_script
|
"requestTimeoutForExecutionMode(executionMode)", app_script
|
||||||
)
|
)
|
||||||
self.assertIn('error.code === "hardware_busy"', app_script)
|
self.assertIn('error.code === "hardware_busy"', app_script)
|
||||||
self.assertIn("BENCH BUSY", app_script)
|
self.assertIn("BENCH BUSY", app_script)
|
||||||
@@ -977,12 +1024,15 @@ operations:
|
|||||||
self.assertIn('id="saveModeToggle"', html)
|
self.assertIn('id="saveModeToggle"', html)
|
||||||
self.assertIn("根据指令创建", html)
|
self.assertIn("根据指令创建", html)
|
||||||
self.assertIn('aria-label="快捷功能模式"', html)
|
self.assertIn('aria-label="快捷功能模式"', html)
|
||||||
self.assertIn('id="modeRailPrev"', html)
|
self.assertIn('id="modeRailContent"', html)
|
||||||
self.assertIn('id="modeRailNext"', html)
|
self.assertIn('aria-label="功能模式列表"', html)
|
||||||
|
self.assertNotIn('id="modeRailPrev"', html)
|
||||||
|
self.assertNotIn('id="modeRailNext"', html)
|
||||||
|
self.assertLess(html.index('id="saveModeToggle"'), html.index('id="modeRailContent"'))
|
||||||
self.assertIn('id="modeGalleryToggle"', html)
|
self.assertIn('id="modeGalleryToggle"', html)
|
||||||
self.assertIn('id="modeGallery"', html)
|
self.assertIn('id="modeGallery"', html)
|
||||||
self.assertIn('id="modeGalleryGroups"', html)
|
self.assertIn('id="modeGalleryGroups"', html)
|
||||||
self.assertIn("全部功能模式", html)
|
self.assertIn("自定义模式", html)
|
||||||
self.assertIn('id="modeEditorBackdrop"', html)
|
self.assertIn('id="modeEditorBackdrop"', html)
|
||||||
self.assertIn('id="modeEditorName"', html)
|
self.assertIn('id="modeEditorName"', html)
|
||||||
self.assertIn('id="modeEditorInstruction"', html)
|
self.assertIn('id="modeEditorInstruction"', html)
|
||||||
@@ -990,7 +1040,7 @@ operations:
|
|||||||
self.assertIn('id="modeEditorComplete"', html)
|
self.assertIn('id="modeEditorComplete"', html)
|
||||||
self.assertIn('id="modeEditorFlowPreview"', html)
|
self.assertIn('id="modeEditorFlowPreview"', html)
|
||||||
self.assertIn('class="mode-editor-flow-preview empty"', html)
|
self.assertIn('class="mode-editor-flow-preview empty"', html)
|
||||||
self.assertIn('id="saveModeSource"', html)
|
self.assertNotIn('id="saveModeSource"', html)
|
||||||
self.assertNotIn("主驾 24℃", html)
|
self.assertNotIn("主驾 24℃", html)
|
||||||
populate_modes = app_script[
|
populate_modes = app_script[
|
||||||
app_script.index("function populateModeChips()")
|
app_script.index("function populateModeChips()")
|
||||||
@@ -998,13 +1048,18 @@ operations:
|
|||||||
]
|
]
|
||||||
self.assertNotIn("ch1_body_can_driver_only", populate_modes)
|
self.assertNotIn("ch1_body_can_driver_only", populate_modes)
|
||||||
self.assertIn("function populateModeGallery", app_script)
|
self.assertIn("function populateModeGallery", app_script)
|
||||||
self.assertIn("function updateModeRailNavigation", app_script)
|
self.assertNotIn("function updateModeRailNavigation", app_script)
|
||||||
self.assertIn("function scrollModeRail", app_script)
|
self.assertNotIn("function scrollModeRail", app_script)
|
||||||
self.assertIn('className = "mode-gallery-card"', app_script)
|
self.assertIn('className = "mode-gallery-card"', app_script)
|
||||||
|
self.assertIn("function createModeGalleryAddCard", app_script)
|
||||||
|
self.assertIn('button.dataset.modeCreate = "true"', app_script)
|
||||||
|
self.assertIn("function openModeEditorForCreate", app_script)
|
||||||
|
self.assertIn('apiRequest(endpoint', app_script)
|
||||||
|
self.assertIn('? "/api/mode-profiles"', app_script)
|
||||||
self.assertIn("#modeGallery button[data-mode-profile-id]", app_script)
|
self.assertIn("#modeGallery button[data-mode-profile-id]", app_script)
|
||||||
self.assertIn("function setModeExecutionState", app_script)
|
self.assertIn("function setModeExecutionState", app_script)
|
||||||
self.assertIn("function requestCommandAndWait", app_script)
|
self.assertNotIn("function requestCommandAndWait", app_script)
|
||||||
self.assertIn("已创建并添加到功能模式栏", app_script)
|
self.assertIn("已永久保存到模式卡片", app_script)
|
||||||
self.assertLess(
|
self.assertLess(
|
||||||
app_script.index('id: "personal"'),
|
app_script.index('id: "personal"'),
|
||||||
app_script.index('id: "frequent"'),
|
app_script.index('id: "frequent"'),
|
||||||
@@ -1039,7 +1094,7 @@ operations:
|
|||||||
self.assertIn('id="cabinCarouselNext"', app_script)
|
self.assertIn('id="cabinCarouselNext"', app_script)
|
||||||
self.assertIn('aria-label="左右浏览设备卡片"', app_script)
|
self.assertIn('aria-label="左右浏览设备卡片"', app_script)
|
||||||
self.assertNotIn('class="cabin-category-tabs"', app_script)
|
self.assertNotIn('class="cabin-category-tabs"', app_script)
|
||||||
self.assertIn('<span class="section-kicker">LIVE VEHICLE DIGITAL TWIN</span>', html)
|
self.assertNotIn('class="section-kicker"', html)
|
||||||
self.assertIn('.joint-twin-shell > .twin-band {', console_styles)
|
self.assertIn('.joint-twin-shell > .twin-band {', console_styles)
|
||||||
self.assertIn('grid-template-rows: 62px minmax(0, 1fr)', console_styles)
|
self.assertIn('grid-template-rows: 62px minmax(0, 1fr)', console_styles)
|
||||||
self.assertIn('grid-template-columns: repeat(2, minmax(0, 1fr))', console_styles)
|
self.assertIn('grid-template-columns: repeat(2, minmax(0, 1fr))', console_styles)
|
||||||
@@ -1327,7 +1382,7 @@ operations:
|
|||||||
) as response:
|
) as response:
|
||||||
extension_script = response.read().decode("utf-8")
|
extension_script = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
f"{self.base_url}/app.js?v=dual-seat-109", timeout=3
|
f"{self.base_url}/app.js?v=dual-seat-114", timeout=3
|
||||||
) as response:
|
) as response:
|
||||||
app_script = response.read().decode("utf-8")
|
app_script = response.read().decode("utf-8")
|
||||||
|
|
||||||
|
|||||||
@@ -621,6 +621,25 @@ operations:
|
|||||||
if item["id"] == "office"
|
if item["id"] == "office"
|
||||||
)
|
)
|
||||||
self.assertEqual(persisted["name"], "专注办公模式")
|
self.assertEqual(persisted["name"], "专注办公模式")
|
||||||
|
created = self.service.create_mode_profile_card(
|
||||||
|
"自然舒享模式",
|
||||||
|
instruction,
|
||||||
|
generated["request_id"],
|
||||||
|
"denza_n7",
|
||||||
|
)
|
||||||
|
self.assertEqual(created["status"], "succeeded")
|
||||||
|
self.assertEqual(created["mode_profile"]["origin"], "user")
|
||||||
|
self.assertEqual(created["mode_profile"]["instruction"], instruction)
|
||||||
|
self.assertNotEqual(created["mode_profile"]["id"], "office")
|
||||||
|
self.assertTrue(
|
||||||
|
(
|
||||||
|
user_root
|
||||||
|
/ "vehicles"
|
||||||
|
/ "denza_n7"
|
||||||
|
/ "user"
|
||||||
|
/ f"{created['mode_profile']['id']}.yaml"
|
||||||
|
).is_file()
|
||||||
|
)
|
||||||
with self.assertRaisesRegex(ValueError, "生成指令"):
|
with self.assertRaisesRegex(ValueError, "生成指令"):
|
||||||
self.service.save_mode_profile_card(
|
self.service.save_mode_profile_card(
|
||||||
"office",
|
"office",
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
WEB = ROOT / "apps" / "web-console"
|
||||||
|
|
||||||
|
|
||||||
|
def source(name: str) -> str:
|
||||||
|
return (WEB / name).read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_shadcn_semantic_theme_tokens_drive_the_console() -> None:
|
||||||
|
styles = source("styles.css")
|
||||||
|
console = source("console-v2.css")
|
||||||
|
speech = source("speech-input.css")
|
||||||
|
|
||||||
|
expected_tokens = {
|
||||||
|
"--background: oklch(1 0 0)",
|
||||||
|
"--foreground: oklch(0.145 0.008 326)",
|
||||||
|
"--primary: oklch(0.488 0.243 264.376)",
|
||||||
|
"--primary-foreground: oklch(0.97 0.014 254.604)",
|
||||||
|
"--destructive: oklch(0.577 0.245 27.325)",
|
||||||
|
"--sidebar-primary: oklch(0.546 0.245 262.881)",
|
||||||
|
"--background: oklch(0.145 0.008 326)",
|
||||||
|
"--foreground: oklch(0.985 0 0)",
|
||||||
|
"--primary: oklch(0.424 0.199 265.638)",
|
||||||
|
"--sidebar-primary: oklch(0.623 0.214 259.815)",
|
||||||
|
}
|
||||||
|
token_lines = {line.strip() for line in styles.replace(";", "").splitlines()}
|
||||||
|
assert expected_tokens <= token_lines
|
||||||
|
assert ".dark {" in styles
|
||||||
|
assert "--v2-accent: var(--primary)" in console
|
||||||
|
assert "--v2-sidebar: var(--sidebar)" in console
|
||||||
|
assert "color: var(--destructive)" in speech
|
||||||
|
|
||||||
|
|
||||||
|
def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None:
|
||||||
|
html = source("index.html")
|
||||||
|
service_worker = source("service-worker.js")
|
||||||
|
|
||||||
|
for asset in (
|
||||||
|
"/styles.css?v=dual-seat-21",
|
||||||
|
"/console-v2.css?v=delivery-state-50",
|
||||||
|
"/self-extension.css?v=gscu-tcp-42",
|
||||||
|
"/speech-input.css?v=sensevoice-5",
|
||||||
|
):
|
||||||
|
assert asset in html
|
||||||
|
assert f'"{asset}"' in service_worker
|
||||||
|
assert 'const CACHE_VERSION = "cognitive-agent-pad-v227"' in service_worker
|
||||||
|
|
||||||
|
|
||||||
|
def test_vehicle_twin_icons_and_nested_overlays_use_semantic_tokens() -> None:
|
||||||
|
styles = source("styles.css")
|
||||||
|
console = source("console-v2.css")
|
||||||
|
extension = source("self-extension.css")
|
||||||
|
|
||||||
|
expected_console_rules = {
|
||||||
|
".console-v2 .v2-twin-hotspot-label",
|
||||||
|
".console-v2 .v2-twin-device-popover",
|
||||||
|
".console-v2 .v2-reasoning-field",
|
||||||
|
".console-v2 .device-power-drawer",
|
||||||
|
".console-v2 .v2-sync-log li[data-log-status=\"error\"] > span",
|
||||||
|
"background: var(--primary)",
|
||||||
|
"background: var(--popover)",
|
||||||
|
}
|
||||||
|
expected_overlay_rules = {
|
||||||
|
".mode-gallery-backdrop",
|
||||||
|
".mode-editor-backdrop",
|
||||||
|
".gesture-drawer",
|
||||||
|
"background: var(--card)",
|
||||||
|
"border-color: var(--border)",
|
||||||
|
}
|
||||||
|
expected_extension_rules = {
|
||||||
|
".v2-extension-dialog::backdrop",
|
||||||
|
".v2-gscu-tcp-dialog",
|
||||||
|
".v2-nfc-dialog",
|
||||||
|
".v2-extension-toast.error",
|
||||||
|
".console-v2 #v2ExtensionPanel .v2-extension-gateway > button",
|
||||||
|
"background: var(--destructive)",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert all(rule in console for rule in expected_console_rules)
|
||||||
|
assert all(rule in styles for rule in expected_overlay_rules)
|
||||||
|
assert all(rule in extension for rule in expected_extension_rules)
|
||||||
|
|
||||||
|
|
||||||
|
def test_operator_controls_share_status_and_typography_tokens() -> None:
|
||||||
|
html = source("index.html")
|
||||||
|
styles = source("styles.css")
|
||||||
|
console = source("console-v2.css")
|
||||||
|
script = source("console-v2.js")
|
||||||
|
|
||||||
|
for token in (
|
||||||
|
"--status-online:",
|
||||||
|
"--status-offline:",
|
||||||
|
"--text-caption:",
|
||||||
|
"--text-control:",
|
||||||
|
"--text-input:",
|
||||||
|
"--text-label:",
|
||||||
|
):
|
||||||
|
assert token in styles
|
||||||
|
assert '<div class="mode-rail" aria-label="功能模式">' in html
|
||||||
|
assert 'id="modeRailToggle"' in html
|
||||||
|
assert 'aria-controls="modeRailContent"' in html
|
||||||
|
assert 'id="modeRailContent"' in html
|
||||||
|
assert 'role="region"' in html
|
||||||
|
assert 'aria-label="功能模式列表"' in html
|
||||||
|
assert '<span>自定义</span>' in html
|
||||||
|
assert "模拟下发" in html
|
||||||
|
assert "真实下发" in html
|
||||||
|
assert 'id="simulationMode"' not in html
|
||||||
|
assert 'class="v2-sync-link-stack"' not in script
|
||||||
|
assert '.mode-option[data-connection-state="online"]::after' in styles
|
||||||
|
assert '.mode-option[data-connection-state="offline"]::after' in styles
|
||||||
|
assert "font-size: var(--text-control) !important" in console
|
||||||
|
|
||||||
|
|
||||||
|
def test_wired_can_link_requires_real_device_connection_evidence() -> None:
|
||||||
|
app = source("app.js")
|
||||||
|
|
||||||
|
assert 'runtime?.state_sync?.state === "online"' in app
|
||||||
|
assert '["online", "ready"].includes(runtime?.state_sync?.state)' not in app
|
||||||
|
assert 'wiredCanProbe: { passenger: false, vehicle: false }' in app
|
||||||
|
assert 'updateWiredCanProbe("passenger", true, requestedTransport)' in app
|
||||||
|
assert 'updateWiredCanProbe("vehicle", true, requestedTransport)' in app
|
||||||
|
assert 'updateWiredCanProbe("passenger", false, requestedTransport)' in app
|
||||||
|
assert 'updateWiredCanProbe("vehicle", false, requestedTransport)' in app
|
||||||
|
assert "resetTransportConnectionEvidence();" in app
|
||||||
|
assert 'label.textContent = `${transportLabel} · 信号链路断开`' in app
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_fields_use_distinct_semantic_colors() -> None:
|
||||||
|
styles = source("styles.css")
|
||||||
|
console = source("console-v2.css")
|
||||||
|
|
||||||
|
for token in (
|
||||||
|
"--command-channel:",
|
||||||
|
"--command-source:",
|
||||||
|
"--command-delivered:",
|
||||||
|
"--command-pending:",
|
||||||
|
"--command-category:",
|
||||||
|
"--command-control:",
|
||||||
|
"--command-frame:",
|
||||||
|
):
|
||||||
|
assert token in styles
|
||||||
|
for mapping in (
|
||||||
|
"color: var(--command-channel)",
|
||||||
|
"color: var(--command-source)",
|
||||||
|
"color: var(--command-pending)",
|
||||||
|
"color: var(--command-delivered)",
|
||||||
|
"color: var(--command-category)",
|
||||||
|
"color: var(--command-control)",
|
||||||
|
"color: var(--command-frame)",
|
||||||
|
):
|
||||||
|
assert mapping in console
|
||||||
|
|
||||||
|
|
||||||
|
def test_function_modes_use_an_inline_accessible_collapsible() -> None:
|
||||||
|
html = source("index.html")
|
||||||
|
app = source("app.js")
|
||||||
|
console = source("console-v2.css")
|
||||||
|
|
||||||
|
assert '<div class="mode-rail-bar">' in html
|
||||||
|
assert 'id="modeRailSelection">常用模式</span>' in html
|
||||||
|
assert 'id="modeGalleryToggle"' in html
|
||||||
|
assert '<span>自定义</span>' in html
|
||||||
|
assert '<details class="mode-rail"' not in html
|
||||||
|
assert 'class="mode-rail-dropdown"' not in html
|
||||||
|
assert 'toggle.setAttribute("aria-expanded", String(open))' in app
|
||||||
|
assert "content.hidden = !open" in app
|
||||||
|
assert 'document.body.classList.toggle("mode-rail-expanded", open)' not in app
|
||||||
|
assert 'document.addEventListener("pointerdown"' in app
|
||||||
|
assert '.v2-mode-rail > .mode-rail-content[hidden]' in console
|
||||||
|
assert ".mode-rail-expanded .main-panel" not in console
|
||||||
|
assert "position: absolute" in console
|
||||||
|
assert "top: calc(100% + 8px)" in console
|
||||||
|
assert "overflow-y: auto" in console
|
||||||
|
assert "scrollbar-gutter: stable" in console
|
||||||
|
|
||||||
|
|
||||||
|
def test_vehicle_device_cards_hide_stale_values_without_signal() -> None:
|
||||||
|
console = source("console-v2.js")
|
||||||
|
|
||||||
|
assert "function normalizeDeviceReadout(value)" in console
|
||||||
|
assert "function deviceFeedbackAvailable(deviceId)" in console
|
||||||
|
assert 'return rows.map(([label]) => [label, "-"])' in console
|
||||||
|
assert 'const waiting = rows.some(([, value]) => value === "-")' in console
|
||||||
|
assert '"cognitive-agent:vehicle-feedback-state"' in console
|
||||||
|
|
||||||
|
|
||||||
|
def test_final_theme_layers_do_not_reintroduce_raw_ui_colors() -> None:
|
||||||
|
sections = (
|
||||||
|
source("styles.css").split("/* Final semantic theme layer", 1)[1],
|
||||||
|
source("console-v2.css").split("/* Final semantic theme cascade", 1)[1],
|
||||||
|
source("self-extension.css").split(
|
||||||
|
"/* Final semantic theme layer for extension surfaces", 1
|
||||||
|
)[1],
|
||||||
|
)
|
||||||
|
raw_color = re.compile(r"#[0-9a-fA-F]{3,8}\b|rgba?\(")
|
||||||
|
|
||||||
|
for section in sections:
|
||||||
|
assert not raw_color.search(section)
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_five_output_modules_are_covered_by_the_final_theme_layer() -> None:
|
||||||
|
console = source("console-v2.css")
|
||||||
|
final_layer = console.split("/* Final semantic theme cascade", 1)[1]
|
||||||
|
|
||||||
|
for selector in (
|
||||||
|
".joint-twin-shell.v2-output-page",
|
||||||
|
".decision-list li",
|
||||||
|
".result-workspace #panel-config.config-panel",
|
||||||
|
".plan-panel .frame-summary",
|
||||||
|
".execution-summary",
|
||||||
|
):
|
||||||
|
assert selector in final_layer
|
||||||
|
|
||||||
|
assert "--config-accent: var(--primary)" in final_layer
|
||||||
|
assert "background: var(--card)" in final_layer
|
||||||
|
assert "border-color: var(--border)" in final_layer
|
||||||
@@ -17,7 +17,7 @@ def test_unified_console_exposes_device_id_based_nfc_authorization() -> None:
|
|||||||
|
|
||||||
assert 'data-open-nfc-binding' in script
|
assert 'data-open-nfc-binding' in script
|
||||||
assert '靠近读卡器后' in script
|
assert '靠近读卡器后' in script
|
||||||
assert 'NFC · DEVICE AUTHORIZATION' in script
|
assert '<h2>NFC 设备授权</h2>' in script
|
||||||
assert 'NFC 设备授权' in script
|
assert 'NFC 设备授权' in script
|
||||||
assert 'api("nfc/status")' in script
|
assert 'api("nfc/status")' in script
|
||||||
assert 'function bindCardToDevice(device, cardUid)' in script
|
assert 'function bindCardToDevice(device, cardUid)' in script
|
||||||
@@ -60,8 +60,8 @@ def test_unified_console_exposes_device_id_based_nfc_authorization() -> None:
|
|||||||
assert '.v2-nfc-command-list' in styles
|
assert '.v2-nfc-command-list' in styles
|
||||||
assert '.v2-nfc-command-option' in styles
|
assert '.v2-nfc-command-option' in styles
|
||||||
assert '.v2-nfc-select-all' in styles
|
assert '.v2-nfc-select-all' in styles
|
||||||
assert 'self-extension.js?v=gscu-tcp-40' in html
|
assert 'self-extension.js?v=gscu-tcp-41' in html
|
||||||
assert 'self-extension.css?v=gscu-tcp-39' in html
|
assert 'self-extension.css?v=gscu-tcp-42' in html
|
||||||
|
|
||||||
|
|
||||||
def test_unified_console_exposes_gscu_tcp_configuration_and_actions() -> None:
|
def test_unified_console_exposes_gscu_tcp_configuration_and_actions() -> None:
|
||||||
@@ -139,4 +139,4 @@ def test_main_parsed_control_panel_renders_validated_infrared_operations() -> No
|
|||||||
assert 'frameLabel: "GSCU1B TCP"' in console
|
assert 'frameLabel: "GSCU1B TCP"' in console
|
||||||
assert "已解析控制指令" in console
|
assert "已解析控制指令" in console
|
||||||
assert 'li[data-can-channel="IR"]' in styles
|
assert 'li[data-can-channel="IR"]' in styles
|
||||||
assert "console-v2.js?v=delivery-state-50" in html
|
assert "console-v2.js?v=delivery-state-54" in html
|
||||||
|
|||||||
Reference in New Issue
Block a user