优化后端

This commit is contained in:
Miayui
2026-09-11 17:56:56 +08:00
parent 7eed11b774
commit 812e67422d
7299 changed files with 1744959 additions and 112 deletions
+178 -9
View File
@@ -92,6 +92,24 @@ const REQUEST_TIMEOUT_MS = 45 * 1000;
const DEFAULT_PHYSICAL_STATE_POLL_INTERVAL_MS = 5000;
const AUTHORITATIVE_CH1_SOURCES = new Set(["physical_can_ch1", "wireless_mqtt_ch1"]);
async function apiResponseError(response) {
let payload = null;
try {
payload = await response.json();
} catch (_error) {
// Some proxies return an empty or non-JSON error page. Preserve the HTTP
// status even when no structured reason is available.
}
const status = Number(response?.status) || 0;
const serverMessage = String(payload?.message || payload?.error || "").trim();
const fallback = status ? `HTTP ${status}` : "请求失败";
const error = new Error(serverMessage ? `${fallback}:${serverMessage}` : fallback);
error.status = status;
error.code = String(payload?.error || "").trim();
error.serverMessage = serverMessage;
return error;
}
function requestTimeoutForExecutionMode(mode) {
return mode === "hardware" ? HARDWARE_REQUEST_TIMEOUT_MS : REQUEST_TIMEOUT_MS;
}
@@ -111,6 +129,8 @@ function renderCabinLiveStatus(networkStatus = state.connectionStatus) {
if (!indicator) return;
const label = $("b", indicator);
if (!label) return;
indicator.removeAttribute("aria-label");
indicator.removeAttribute("title");
const summarySystem = $("#cabinSummarySystem");
const setSummarySystem = (text, status) => {
if (!summarySystem) return;
@@ -144,6 +164,22 @@ function renderCabinLiveStatus(networkStatus = state.connectionStatus) {
}
const snapshot = state.vehicleStateSnapshot;
if (snapshot?.errorStatus === 403) {
const reason = snapshot.errorReason || "当前终端没有车辆状态读取权限";
const visibleReason = `${transportLabel} · 403:${reason}`;
indicator.dataset.state = "offline";
label.textContent = visibleReason;
indicator.title = visibleReason;
indicator.setAttribute("aria-label", visibleReason);
const telemetryState = $("#vehicleTelemetryState");
if (telemetryState) telemetryState.textContent = "403 拒绝";
setSummarySystem("403 · 状态读取被拒绝", "warning");
return;
}
const telemetryState = $("#vehicleTelemetryState");
if (telemetryState?.textContent === "403 拒绝") {
telemetryState.textContent = "只读";
}
const observedIds = Array.isArray(snapshot?.observedFrameIds)
? snapshot.observedFrameIds
: [];
@@ -5884,19 +5920,24 @@ function installModeChipScroller() {
let pointerStartScroll = 0;
let dragged = false;
let suppressClick = false;
let pointerCaptured = false;
group.addEventListener("pointerdown", (event) => {
if (event.pointerType !== "mouse" || event.button !== 0 || maximumScroll() <= 1) return;
pointerId = event.pointerId;
pointerStartX = event.clientX;
pointerStartScroll = group.scrollLeft;
dragged = false;
group.setPointerCapture?.(pointerId);
pointerCaptured = false;
});
group.addEventListener("pointermove", (event) => {
if (pointerId !== event.pointerId) return;
const distance = event.clientX - pointerStartX;
if (!dragged && Math.abs(distance) < 5) return;
dragged = true;
if (!pointerCaptured && typeof group.setPointerCapture === "function") {
group.setPointerCapture(pointerId);
pointerCaptured = true;
}
group.classList.add("is-dragging");
group.scrollLeft = Math.max(0, Math.min(maximumScroll(), pointerStartScroll - distance));
updateScrollState();
@@ -5904,8 +5945,9 @@ function installModeChipScroller() {
});
const finishPointerDrag = (event) => {
if (pointerId !== event.pointerId) return;
group.releasePointerCapture?.(pointerId);
if (pointerCaptured) group.releasePointerCapture?.(pointerId);
pointerId = null;
pointerCaptured = false;
group.classList.remove("is-dragging");
if (dragged) {
suppressClick = true;
@@ -6094,11 +6136,113 @@ function installModeActionPreview() {
window.addEventListener("resize", () => hideModeActionPopover({ immediate: true }), { passive: true });
}
function vueModeChipEntry(mode) {
const timeline = mode?.timeline;
const actionCount = modeExecutionActionViews(mode).length
|| Number(mode?.operation_count || 0);
const phaseSummary = Array.isArray(timeline?.phases)
? timeline.phases.map((phase) => {
if (Number(phase.duration_ms || 0) > 0) {
return `${phase.name} ${modeSecondsLabel(phase.duration_ms)}s`;
}
const timeout = Number(phase.timeout_ms || 0);
return timeout > 0
? `${phase.name} 目标到位(保护上限${modeSecondsLabel(timeout)}s)`
: `${phase.name} 目标到位`;
}).join(" → ")
: "";
let cycleLabel = "";
if (["infinite", "until_stopped_or_timeout"].includes(timeline?.repeat)) {
const runtimeMinutes = Number(timeline.max_runtime_ms || 0) / 60000;
const runtimeLabel = Number.isInteger(runtimeMinutes)
? String(runtimeMinutes)
: runtimeMinutes.toFixed(1);
const cycleDurationMs = Number(timeline.cycle_duration_ms || 0);
const cycle = cycleDurationMs > 0
? `${modeSecondsLabel(cycleDurationMs)}s循环`
: "目标到位循环";
cycleLabel = runtimeMinutes > 0 ? `${cycle} · 最长${runtimeLabel}min` : cycle;
}
return {
type: "mode",
key: `mode-${mode.id}`,
id: mode.id,
label: modeChipLabel(mode),
summary: modeChipSummary(mode),
icon: mode.icon || "bookmark",
command: `进入${mode.name}`,
origin: mode?.origin || "preset",
behavior: cycleLabel ? "cyclic" : "",
cycleLabel,
ariaLabel: `${mode.name || mode.id},${actionCount} 项执行动作,点击执行`,
title: [mode.description || mode.name, phaseSummary, "悬浮或聚焦查看全部执行动作"]
.filter(Boolean)
.join("\n"),
};
}
function vueModeChipEntries(modes) {
const preferredStaticOrder = [
"zero_gravity",
"office",
"nap",
"queen_passenger_driving",
"leisure_lounger",
];
const userModes = modes.filter(isUserModeProfile);
const dynamicOrder = new Map([["rocking_horse", 0], ["dynamic_cradle", 1]]);
const dynamicModes = modes
.filter((mode) => !isUserModeProfile(mode) && isDynamicModeProfile(mode))
.sort((left, right) => (
(dynamicOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER)
- (dynamicOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER)
));
const staticModes = modes.filter(
(mode) => !isUserModeProfile(mode) && !isDynamicModeProfile(mode),
);
const preferredStaticModes = preferredStaticOrder
.map((modeId) => staticModes.find((mode) => mode.id === modeId))
.filter(Boolean);
const otherStaticModes = staticModes.filter((mode) => !preferredStaticModes.includes(mode));
const entries = [];
const appendLabel = (label, id) => entries.push({
type: "label",
key: `label-${id}`,
id,
label,
});
const appendDivider = (id) => entries.push({
type: "divider",
key: `divider-${id}`,
id,
});
if (userModes.length) {
appendLabel("自定义模式", "custom");
entries.push(...userModes.map(vueModeChipEntry));
}
if (userModes.length && (staticModes.length || dynamicModes.length)) appendDivider("custom");
if (staticModes.length) {
appendLabel("静态模式", "static");
entries.push(...preferredStaticModes.map(vueModeChipEntry));
entries.push(...otherStaticModes.map(vueModeChipEntry));
}
if (dynamicModes.length) {
if (userModes.length || staticModes.length) appendDivider("dynamic");
appendLabel("动态模式", "dynamic");
entries.push(...dynamicModes.map(vueModeChipEntry));
}
return entries;
}
function populateModeChips() {
const group = $(".mode-chips");
if (!group) return;
const buttons = () => $$("button", group);
const modes = modeProfiles();
if (window.CognitiveAgentVueUI?.renderModeChips?.(group, vueModeChipEntries(modes))) {
populateModeGallery();
return;
}
const runtimeModeIds = new Set(modes.map((mode) => mode.id));
$$('button[data-mode-origin="user"]', group).forEach((button) => {
if (!runtimeModeIds.has(button.dataset.modeProfileId)) button.remove();
@@ -6155,6 +6299,8 @@ function populateModeChips() {
runIcon.dataset.lucide = "play";
button.append(runIcon);
}
runIcon.dataset.modeRun = "true";
runIcon.title = `执行${mode.name || mode.id}`;
const timeline = mode.timeline;
let cycleBadge = $(".mode-cycle-badge", button);
if (["infinite", "until_stopped_or_timeout"].includes(timeline?.repeat)) {
@@ -6330,8 +6476,10 @@ function setModeChipsDisabled(disabled) {
});
}
async function executeSavedModeProfile(mode) {
async function executeSavedModeProfile(mode, { command = "" } = {}) {
if (!mode?.id || !mode?.command_flow?.source_request_id) return false;
const modeCommand = String(command || `进入${mode.name || mode.id}`).trim();
const assistant = window.CognitiveAgentAssistant;
const driverOnlyCh1 = isDriverOnlyCh1Runtime();
if (state.executionMode === "hardware"
&& state.controlTransport !== "wireless_mqtt"
@@ -6346,10 +6494,13 @@ async function executeSavedModeProfile(mode) {
closeModeGallery({ restoreFocus: false });
const runButton = $("#runButton");
const input = $("#utterance");
runButton.disabled = true;
setModeChipsDisabled(true);
setModeExecutionState(mode.id, "running");
$("#utterance").value = mode.name || mode.id;
assistant?.open?.();
input.value = modeCommand;
input.dispatchEvent(new Event("input", { bubbles: true }));
setRunStatus("processing", "MODE COMMAND FLOW");
$("#resultTitle").textContent = `正在直接执行${mode.name}`;
try {
@@ -6375,11 +6526,11 @@ async function executeSavedModeProfile(mode) {
if (!payload) throw new Error("模式卡片未返回执行结果");
renderResult(payload);
const completed = ["succeeded", "partially_succeeded"].includes(payload.status);
$("#utterance").value = mode.name || mode.id;
$("#resultTitle").textContent = completed
? `${mode.name}执行完成`
: `${mode.name}执行未完成`;
setModeExecutionState(mode.id, completed ? "succeeded" : "failed");
assistant?.announceModeExecution?.(mode, payload, { command: modeCommand });
if (state.executionMode === "hardware") await synchronizePhysicalSeatState();
showToast(completed
? `${mode.name}已直接执行保存的控制指令流`
@@ -6388,8 +6539,13 @@ async function executeSavedModeProfile(mode) {
setModeExecutionState(mode.id, "failed");
setRunStatus("failed", "MODE FAILED");
$("#resultTitle").textContent = `${mode.name}执行失败`;
assistant?.announceModeExecution?.(mode, null, { command: modeCommand, error });
showToast(error.message || "模式执行失败");
} finally {
if (input.value === modeCommand) {
input.value = "";
input.dispatchEvent(new Event("input", { bubbles: true }));
}
runButton.disabled = false;
setModeChipsDisabled(false);
}
@@ -6412,6 +6568,10 @@ function applyModeCommand(command, modeProfileId = null) {
setModeRailSelection(mode);
closeModeGallery({ restoreFocus: false });
assistant?.open?.();
if (mode?.command_flow?.source_request_id) {
void executeSavedModeProfile(mode, { command: modeCommand });
return true;
}
const input = $("#utterance");
const form = $("#requestForm");
if (!input || !form) {
@@ -6468,7 +6628,7 @@ function applyRuntimeTargetPolicy(runtime) {
async function loadRuntime() {
try {
const response = await apiRequest("/api/runtime", { cache: "no-store" });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
if (!response.ok) throw await apiResponseError(response);
state.runtime = await response.json();
renderControlTransport();
applyRuntimeTargetPolicy(state.runtime);
@@ -6487,6 +6647,7 @@ async function loadRuntime() {
mqttDualChannel ? vehicleCan.configured : hardware.configured,
);
const vehicleHardwareReady = Boolean(vehicleCan.configured);
const linkedHardwareReady = seatHardwareReady && vehicleHardwareReady;
const ch1DriverOnly = vehicleHardwareReady && !seatHardwareReady;
const hardwareExecutionReady = Boolean(hardware.configured || vehicleCan.configured);
const gptButton = $('[data-provider="openai"]');
@@ -6563,7 +6724,7 @@ async function synchronizePassengerSeatState() {
window.location.replace("/login");
return false;
}
if (!response.ok) throw new Error(`HTTP ${response.status}`);
if (!response.ok) throw await apiResponseError(response);
const payload = await response.json();
if (requestedTransport !== state.controlTransport) return false;
updateWiredCanProbe("passenger", true, requestedTransport);
@@ -6605,7 +6766,7 @@ async function synchronizeDriverSeatState() {
window.location.replace("/login");
return false;
}
if (!response.ok) throw new Error(`HTTP ${response.status}`);
if (!response.ok) throw await apiResponseError(response);
const payload = await response.json();
if (requestedTransport !== state.controlTransport) return false;
updateWiredCanProbe("vehicle", true, requestedTransport);
@@ -6627,6 +6788,9 @@ async function synchronizeDriverSeatState() {
state.vehicleStateSnapshot = {
...(state.vehicleStateSnapshot || {}),
error: error.message || String(error),
errorStatus: Number(error.status) || null,
errorCode: error.code || "",
errorReason: error.serverMessage || error.message || String(error),
};
renderCabinLiveStatus();
if (!["physical_can_ch1", "wireless_mqtt_ch1"].includes(state.seatFeedbackSources.driver)) {
@@ -7950,6 +8114,10 @@ function recordVehicleStateTelemetry(payload) {
: "physical_can_ch1";
state.vehicleStateSnapshot = {
...previous,
error: null,
errorStatus: null,
errorCode: "",
errorReason: "",
source,
capturedAt: payload.captured_at || null,
receivedAt: Date.now(),
@@ -8716,7 +8884,8 @@ document.addEventListener("DOMContentLoaded", async () => {
installModeActionPreview();
$(".mode-chips").addEventListener("click", (event) => {
const target = event.target instanceof Element ? event.target : null;
const button = target?.closest("button[data-mode-command]");
const runControl = target?.closest("[data-mode-run]");
const button = (runControl || target)?.closest("button[data-mode-command]");
if (button) applyModeCommand(
button.dataset.modeCommand,
button.dataset.modeProfileId || null,
+217
View File
@@ -1506,3 +1506,220 @@
min-height: 32px !important;
}
}
/* Compact command receipt inside each AI assistant response. */
.assistant-message-with-dispatched-controls {
width: 100%;
max-width: none;
align-self: stretch;
}
.assistant-message-with-dispatched-controls .assistant-message-body {
min-width: 0;
flex: 1 1 0;
}
.assistant-dispatched-controls-vue-host {
width: 100%;
max-width: 336px;
}
.assistant-dispatched-controls {
width: 100%;
max-width: 336px;
box-sizing: border-box;
overflow: hidden;
color: var(--assistant-text, #233044);
background: color-mix(in oklch, var(--card) 91%, var(--accent));
border: 1px solid color-mix(in oklch, var(--primary) 18%, var(--border));
border-radius: 14px;
box-shadow: 0 8px 24px color-mix(in oklch, var(--foreground) 7%, transparent);
}
.assistant-dispatched-controls > summary {
min-height: 46px;
display: grid;
grid-template-columns: 30px minmax(0, 1fr) auto 18px;
align-items: center;
gap: 8px;
padding: 7px 11px;
color: var(--heading-foreground, #1e2f46);
cursor: pointer;
list-style: none;
user-select: none;
}
.assistant-dispatched-controls > summary::-webkit-details-marker {
display: none;
}
.assistant-dispatched-controls > summary:focus-visible {
outline: 2px solid color-mix(in oklch, var(--primary) 42%, transparent);
outline-offset: -2px;
}
.assistant-dispatched-controls-heading-icon,
.assistant-dispatched-control-icon {
display: inline-grid;
place-items: center;
color: var(--primary, #2771e7);
}
.assistant-dispatched-controls-heading-icon {
width: 30px;
height: 30px;
background: color-mix(in oklch, var(--primary) 9%, var(--card));
border-radius: 9px;
}
.assistant-dispatched-controls-heading-icon svg {
width: 16px;
height: 16px;
stroke-width: 1.9;
}
.assistant-dispatched-controls > summary > b {
min-width: 0;
overflow: hidden;
font-size: 13px;
font-weight: 850;
text-overflow: ellipsis;
white-space: nowrap;
}
.assistant-dispatched-controls-count {
min-width: 25px;
height: 22px;
display: inline-grid;
place-items: center;
padding: 0 7px;
color: var(--primary, #2771e7);
background: color-mix(in oklch, var(--primary) 10%, var(--card));
border-radius: 999px;
font-size: 10px;
font-weight: 850;
}
.assistant-dispatched-controls-chevron {
width: 17px;
height: 17px;
color: var(--muted-foreground, #67768b);
transition: transform 160ms ease;
}
.assistant-dispatched-controls[open] > summary .assistant-dispatched-controls-chevron {
transform: rotate(180deg);
}
.assistant-dispatched-controls-list {
margin: 0;
padding: 0;
list-style: none;
border-top: 1px solid color-mix(in oklch, var(--primary) 17%, var(--border));
}
.assistant-dispatched-controls-list > li {
min-height: 55px;
display: grid;
grid-template-columns: 30px minmax(0, 1fr) auto;
align-items: center;
gap: 9px;
padding: 8px 12px;
}
.assistant-dispatched-controls-list > li + li {
border-top: 1px solid color-mix(in oklch, var(--primary) 17%, var(--border));
}
.assistant-dispatched-control-icon {
width: 30px;
height: 30px;
}
.assistant-dispatched-control-icon svg {
width: 17px;
height: 17px;
stroke-width: 1.8;
}
.assistant-dispatched-control-label {
min-width: 0;
overflow: hidden;
color: var(--muted-foreground, #5b687a);
font-size: 12px;
font-weight: 680;
text-overflow: ellipsis;
white-space: nowrap;
}
.assistant-dispatched-control-value {
max-width: 112px;
overflow: hidden;
color: var(--heading-foreground, #26364b);
font-size: 12px;
font-weight: 850;
text-overflow: ellipsis;
white-space: nowrap;
}
/* The function-mode strip is now the only homepage header. */
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .main-panel,
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync.assistant-drawer-open .main-panel {
grid-template-rows: auto minmax(0, 1fr);
}
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .main-panel > .v2-mode-rail {
grid-column: 1 / -1;
grid-row: 1;
}
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .v2-output-shell,
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync.assistant-drawer-open .v2-output-shell {
grid-row: 2;
}
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .main-panel > .v2-mode-rail .v2-module-switcher,
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync.assistant-drawer-open .main-panel > .v2-mode-rail .v2-module-switcher {
width: auto;
min-width: max-content;
min-height: 0;
position: static;
inset: auto;
z-index: auto;
display: block;
grid-column: auto;
grid-row: auto;
gap: 0;
margin: 0;
padding: 0;
background: transparent;
border: 0;
box-shadow: none;
backdrop-filter: none;
}
@media (min-width: 901px) {
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync.assistant-drawer-open .v2-operation-shell {
grid-row: 2;
}
}
@media (max-width: 900px) {
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .main-panel {
grid-template-rows: auto minmax(0, 1fr);
}
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .v2-output-shell {
grid-row: 2;
}
}
@media (max-width: 760px) {
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .v2-operation-shell {
top: 108px;
}
.assistant-dispatched-controls {
max-width: none;
}
}
+317
View File
@@ -502,6 +502,282 @@
followLatest(true);
}
const assistantControlLabels = {
"climate.set_power": "空调电源",
"climate.set_temperature": "空调温度",
"climate.set_fan_level": "空调风量",
"climate.set_outlet_mode": "出风模式",
"climate.set_control_mode": "空调控制方式",
"climate.set_cycle_mode": "空调循环",
"interior_light.set_indoor_power": "室内灯",
"interior_light.set_reading_power": "阅读灯",
"ambient_light.set_power": "氛围灯",
"ambient_light.set_color_index": "氛围灯颜色",
"ambient_light.set_brightness_level": "氛围灯亮度",
"ambient_light.set_illumination": "氛围灯颜色与亮度",
"ambient_light.set_mode": "氛围灯模式",
"roof.set_shade_motion": "遮阳帘",
"roof.set_shade_position": "遮阳帘位置",
"window.set_motion": "车窗",
"window.set_position": "车窗位置",
"door.set_rear_lock": "后排门锁",
"door.set_vehicle_lock": "整车门锁",
"infrared.emit": "红外外设",
};
function assistantControlCategory(operation = {}, fallback = "控制指令") {
const capability = String(operation.capability || "");
if (capability === "infrared.emit") {
const deviceName = String(
operation.target?.name || operation.target?.device || "红外外设",
).trim();
return deviceName || "红外外设";
}
if (assistantControlLabels[capability]) return assistantControlLabels[capability];
if (capability.startsWith("seat.")) {
const target = String(operation.target?.zone || operation.target?.instance || "")
.replaceAll("_", "-");
return `${["passenger", "front-right"].includes(target) ? "副驾" : "主驾"}座椅`;
}
if (capability.startsWith("outlet.")) return "空调风口";
return String(fallback || capability || "控制指令");
}
function fallbackAssistantControlCommands(result = {}) {
return (Array.isArray(result.validated_control_commands)
? result.validated_control_commands
: [])
.map((presentation) => {
const operation = presentation?.operation || {};
const command = String(presentation?.actual_command || "").trim();
if (!command) return null;
const capability = String(operation.capability || "");
const target = operation.target || {};
const targetValue = target.zone || target.instance || target.device;
const semanticArguments = [];
if (targetValue != null && String(targetValue).trim()) {
semanticArguments.push(`target=${String(targetValue).replaceAll("_", "-")}`);
}
Object.entries(operation.parameters || {})
.sort(([left], [right]) => left.localeCompare(right))
.forEach(([key, value]) => semanticArguments.push(`${key}=${String(value)}`));
return {
category: assistantControlCategory(operation, capability),
channel: String(presentation?.channel || ""),
command,
deliveryState: "not_submitted",
operation,
operationRef: String(operation.id || ""),
semantic: `${capability || "control"}(${semanticArguments.join(", ")})`,
};
})
.filter(Boolean);
}
function assistantDispatchedCommands(result = {}) {
const projections = Array.isArray(result.validated_control_commands)
? result.validated_control_commands
: [];
const projectionByRef = new Map(projections.map((presentation) => [
String(presentation?.operation?.id || ""),
presentation?.operation || {},
]));
let commands = [];
try {
const sharedCommands = window.CognitiveAgentConsoleV2?.getParsedCanCommands?.(result);
if (Array.isArray(sharedCommands)) commands = sharedCommands;
} catch (_error) {
commands = [];
}
if (!commands.length) return fallbackAssistantControlCommands(result);
return commands.map((item, index) => {
const operation = projectionByRef.get(String(item.operationRef || ""))
|| projections[index]?.operation
|| {};
return {
...item,
category: assistantControlCategory(operation, item.category),
operation,
};
});
}
function localizedAssistantControlValue(value) {
const normalized = String(value ?? "").trim().toLowerCase();
return {
on: "开启",
open: "开启",
true: "开启",
off: "关闭",
close: "关闭",
false: "关闭",
half: "一半",
stop: "停止",
lock: "上锁",
unlock: "解锁",
auto: "自动",
manual: "手动",
internal: "内循环",
external: "外循环",
}[normalized] || String(value ?? "").trim();
}
function assistantControlValue(item = {}) {
const parameters = item.operation?.parameters || {};
const numericValue = (value) => (
value !== null && value !== undefined && value !== "" && Number.isFinite(Number(value))
? Number(value)
: null
);
const temperature = numericValue(parameters.temperature_c);
if (temperature !== null) return `${temperature}°C`;
const position = numericValue(parameters.position_pct ?? parameters.shade_position_pct);
if (position !== null) return `${position}%`;
const illumination = String(parameters.color_hex || "").trim();
const brightnessPercent = numericValue(parameters.brightness_pct);
if (illumination && brightnessPercent !== null) return `${illumination} · ${brightnessPercent}%`;
if (illumination) return illumination;
if (brightnessPercent !== null) return `${brightnessPercent}%`;
const brightness = numericValue(parameters.brightness_level);
if (brightness !== null) return `${brightness}级`;
const level = numericValue(parameters.level ?? parameters.fan_level);
if (level !== null) return `${level}档`;
const state = parameters.motion
?? parameters.state
?? parameters.power
?? parameters.action
?? parameters.mode;
if (state !== null && state !== undefined && String(state).trim()) {
return localizedAssistantControlValue(state);
}
const commandTail = String(item.command || "").trim().split(/\s+/).at(-1);
return localizedAssistantControlValue(commandTail) || String(item.command || "控制");
}
function assistantControlIcon(item = {}) {
const capability = String(item.operation?.capability || "");
const category = String(item.category || "");
if (capability.startsWith("climate.") || category.includes("空调")) return "thermometer";
if (capability.includes("reading") || category.includes("阅读灯")) return "book-open";
if (capability.includes("light") || category.includes("灯")) return "lightbulb";
if (capability.startsWith("roof.") || category.includes("遮阳帘")) return "panel-top";
if (capability.startsWith("window.") || category.includes("车窗")) return "panels-top-left";
if (capability.startsWith("seat.") || category.includes("座椅")) return "armchair";
if (capability.startsWith("door.") || category.includes("门锁")) return "lock-keyhole";
if (capability === "infrared.emit") return "radio-tower";
return "binary";
}
function appendDispatchedControls(message, result = {}) {
if (!message?.body || !result || typeof result !== "object") return;
const previous = message.dispatchedControlResult || {};
const mergedResult = { ...previous, ...result };
if (!Array.isArray(result.validated_control_commands)
&& Array.isArray(previous.validated_control_commands)) {
mergedResult.validated_control_commands = previous.validated_control_commands;
}
message.dispatchedControlResult = mergedResult;
const commands = assistantDispatchedCommands(mergedResult);
if (!commands.length) return;
const signature = JSON.stringify(commands.map((item) => [
item.operationRef,
item.category,
item.command,
item.semantic,
item.deliveryState,
item.deliveryLabel,
item.deliveryDetail,
]));
const vueRenderer = window.CognitiveAgentVueUI?.renderDispatchedControls;
if (typeof vueRenderer === "function") {
let host = $(".assistant-dispatched-controls-vue-host", message.body);
if (host?.dataset.commandSignature === signature) return;
if (!host) {
host = document.createElement("div");
host.className = "assistant-dispatched-controls-vue-host";
message.body.append(host);
}
const viewCommands = commands.map((item, index) => ({
key: item.operationRef || `${item.semantic || item.command}-${index}`,
category: String(item.category || "控制指令"),
value: assistantControlValue(item),
icon: assistantControlIcon(item),
semantic: String(item.semantic || ""),
command: String(item.command || ""),
deliveryState: String(item.deliveryState || "not_submitted"),
deliveryLabel: String(item.deliveryLabel || "未下发"),
deliveryDetail: String(item.deliveryDetail || ""),
}));
if (vueRenderer(host, viewCommands)) {
host.dataset.commandSignature = signature;
message.article?.classList.add("assistant-message-with-dispatched-controls");
followLatest(true);
return;
}
host.remove();
}
let card = $(".assistant-dispatched-controls", message.body);
if (card?.dataset.commandSignature === signature) return;
if (!card) {
card = document.createElement("details");
card.className = "assistant-dispatched-controls";
card.open = true;
const summary = document.createElement("summary");
const headingIcon = document.createElement("span");
headingIcon.className = "assistant-dispatched-controls-heading-icon";
headingIcon.innerHTML = '<i data-lucide="list-tree"></i>';
const heading = document.createElement("b");
heading.textContent = "下发控制指令";
const count = document.createElement("strong");
count.className = "assistant-dispatched-controls-count";
const chevron = document.createElement("i");
chevron.className = "assistant-dispatched-controls-chevron";
chevron.setAttribute("data-lucide", "chevron-down");
summary.append(headingIcon, heading, count, chevron);
const list = document.createElement("ol");
list.className = "assistant-dispatched-controls-list";
card.append(summary, list);
message.body.append(card);
}
card.dataset.commandSignature = signature;
const count = $(".assistant-dispatched-controls-count", card);
const list = $(".assistant-dispatched-controls-list", card);
if (count) count.textContent = String(commands.length);
if (!list) return;
list.replaceChildren();
commands.forEach((item) => {
const row = document.createElement("li");
row.dataset.deliveryState = String(item.deliveryState || "not_submitted");
const titleParts = [];
if (item.semantic) titleParts.push(`语义控制指令:${item.semantic}`);
if (item.command) titleParts.push(`实际控制指令:${item.command}`);
if (item.deliveryLabel) {
titleParts.push(`下发状态:${item.deliveryLabel}${item.deliveryDetail ? `(${item.deliveryDetail})` : ""}`);
}
row.title = titleParts.join("\n");
const icon = document.createElement("span");
icon.className = "assistant-dispatched-control-icon";
icon.innerHTML = `<i data-lucide="${assistantControlIcon(item)}"></i>`;
const label = document.createElement("span");
label.className = "assistant-dispatched-control-label";
label.textContent = String(item.category || "控制指令");
const value = document.createElement("strong");
value.className = "assistant-dispatched-control-value";
value.textContent = assistantControlValue(item);
const accessibleCommand = document.createElement("small");
accessibleCommand.className = "visually-hidden";
accessibleCommand.textContent = titleParts.join(";");
row.append(icon, label, value, accessibleCommand);
list.append(row);
});
message.article?.classList.add("assistant-message-with-dispatched-controls");
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
followLatest(true);
}
function appendModeCreatedState(message, profile, { updated = false } = {}) {
if (!message?.body || !profile?.id) return;
const selector = `[data-assistant-mode-created="${CSS.escape(String(profile.id))}"]`;
@@ -921,6 +1197,7 @@
"cognitive-agent:validated-can-commands",
{ detail: event.payload },
));
appendDispatchedControls(message, event.payload);
return;
}
if (event.type === "answer.delta") {
@@ -952,6 +1229,7 @@
);
if (event.payload) {
window.CognitiveAgentConsole?.applySyncedResult?.(event.payload);
appendDispatchedControls(message, event.payload);
if (
event.payload.mode_profile_draft
&& event.payload.orchestration_route === "mode_profile_create_preview"
@@ -1165,6 +1443,44 @@
}
}
function announceModeExecution(profile, result = null, { command = "", error = null } = {}) {
const modeName = String(profile?.name || profile?.id || "功能模式");
const commandText = String(command || `进入${modeName}`).trim();
const status = String(result?.status || "");
const dryRun = result?.execution_mode === "dry_run";
const waitingForFeedback = result?.execution_outcome === "submitted_unverified";
setDrawerOpen(true);
createMessage("user", commandText);
let content = `${modeName}执行未完成,请查看控制指令状态。`;
let statusText = "功能模式执行未完成";
let statusState = "failed";
if (error) {
content = `${modeName}执行失败:${error.message || "请稍后重试。"}`;
statusText = "功能模式执行失败";
} else if (status === "succeeded") {
content = dryRun
? `已完成模拟编排:${commandText}。`
: waitingForFeedback
? `已提交执行:${commandText},正在等待车辆状态确认。`
: `已完成执行:${commandText}。`;
statusText = dryRun
? "功能模式模拟编排已完成"
: waitingForFeedback
? "模式指令已提交,等待车辆状态确认"
: "功能模式执行已完成";
statusState = "success";
} else if (status === "partially_succeeded") {
content = `${modeName}仅部分完成,请查看未完成的控制指令。`;
statusText = "功能模式部分完成";
}
const message = createMessage("assistant", content);
setActionStatus(message, statusText, statusState);
if (result) appendDispatchedControls(message, result);
persistMessages();
}
function renderInitialMessages() {
const container = $("#assistantMessages");
if (!container) return;
@@ -1221,6 +1537,7 @@
open: () => setDrawerOpen(true),
close: () => setDrawerOpen(false),
startModeCreation,
announceModeExecution,
announceModeCreated(profile) {
if (!profile?.id) return;
setDrawerOpen(true);
+81
View File
@@ -9637,6 +9637,87 @@ body.device-power-drawer-open {
border-left-color: color-mix(in oklch, var(--primary) 52%, var(--border));
}
/* Unified homepage header: panel controls and function modes share one row. */
.console-v2.v2-passive-display.v2-interactive-sync .main-panel {
grid-template-rows: auto minmax(0, 1fr);
}
.console-v2.v2-passive-display.v2-interactive-sync .main-panel > .v2-mode-rail {
grid-column: 1 / -1;
grid-row: 1;
}
.console-v2.v2-passive-display.v2-interactive-sync .main-panel > .v2-output-shell {
grid-row: 2;
}
.console-v2 .main-panel > .v2-mode-rail > .mode-rail-bar {
grid-template-columns: auto auto minmax(0, 1fr) auto;
}
.console-v2.v2-passive-display.v2-interactive-sync .main-panel > .v2-mode-rail .v2-module-switcher {
width: auto;
min-width: max-content;
min-height: 0;
position: static;
inset: auto;
z-index: auto;
display: block;
grid-column: auto;
grid-row: auto;
gap: 0;
margin: 0;
padding: 0;
overflow: visible;
background: transparent;
border: 0;
box-shadow: none;
backdrop-filter: none;
}
.console-v2 .main-panel > .v2-mode-rail.is-collapsed > .mode-rail-bar {
grid-template-columns: auto minmax(0, 1fr) auto;
}
@media (max-width: 980px) {
.console-v2 .main-panel > .v2-mode-rail > .mode-rail-bar {
grid-template-columns: auto auto minmax(0, 1fr) auto;
}
.console-v2 .main-panel > .v2-mode-rail .mode-rail-actions {
grid-column: auto;
}
}
@media (max-width: 760px) {
.console-v2 .main-panel > .v2-mode-rail > .mode-rail-bar,
.console-v2 .main-panel > .v2-mode-rail.is-collapsed > .mode-rail-bar {
grid-template-columns: auto minmax(0, 1fr) auto;
grid-template-rows: auto auto;
}
.console-v2.v2-passive-display.v2-interactive-sync .main-panel > .v2-mode-rail .v2-module-switcher {
grid-column: 1 / -1;
grid-row: 1;
justify-self: start;
}
.console-v2 .main-panel > .v2-mode-rail .mode-rail-identity {
grid-column: 1;
grid-row: 2;
}
.console-v2 .main-panel > .v2-mode-rail > .mode-rail-bar > .mode-rail-content {
grid-column: 2;
grid-row: 2;
}
.console-v2 .main-panel > .v2-mode-rail .mode-rail-actions {
grid-column: 3;
grid-row: 2;
}
}
@media (max-width: 1120px) {
.console-v2 .v2-mode-rail > .mode-rail-bar {
grid-template-columns: auto minmax(150px, 1fr) auto;
+73 -19
View File
@@ -373,6 +373,19 @@
if (new Set(["driver", "passenger", "rear"]).has(target) && temperature !== null) {
return command("空调温度", `ac temp ${target} ${temperature}`);
}
const vehicleZones = new Set([
"vehicle",
"front-row",
"rear-row",
"rear-left",
"rear-right",
]);
if (vehicleZones.has(target) && temperature !== null) {
return command(
"空调温度",
`Climate_Control_064_04(${target}=${temperature}°C)`,
);
}
}
if (capability === "climate.set_control_mode") {
const mode = String(parameters.mode ?? parameters.control_mode ?? "");
@@ -466,6 +479,22 @@
const level = boundedInteger(parameters.brightness_level, 1, 5);
if (level !== null) return command("氛围灯", `light ambient brightness ${level}`);
}
if (capability === "ambient_light.set_illumination") {
const color = String(parameters.color_hex || "").trim().toUpperCase();
const brightness = boundedInteger(parameters.brightness_pct, 0, 100);
if (/^#[0-9A-F]{6}$/.test(color) && brightness !== null) {
return command(
"氛围灯颜色与亮度",
`Ambient_RGB_Control_32B_2B(color=${color}, brightness=${brightness}%)`,
);
}
}
if (capability === "ambient_light.set_mode") {
const mode = String(parameters.mode || "").trim();
if (new Set(["camping", "rest", "cinema", "meditation", "romantic", "off", "on"]).has(mode)) {
return command("氛围灯模式", `Ambient_Mode_Control_496_04(mode=${mode})`);
}
}
if (capability === "roof.set_shade_motion") {
const motion = String(parameters.motion || "");
if (new Set(["open", "close", "half", "stop"]).has(motion)) {
@@ -526,13 +555,39 @@
|| telemetry.feedback_observed === true
|| telemetry.physical_effect_confirmed === true
);
const failureMessage = String(execution?.message || "").trim();
const executionFailed = execution?.status === "failed"
|| telemetry.verification_status === "failed_or_blocked";
const safetyBlocked = executionFailed && !submitted && (
/no fresh valid 0x41a shade position/i.test(failureMessage)
|| /refusing (?:blind )?motion/i.test(failureMessage)
|| /thermal protection/i.test(failureMessage)
);
const deliveryState = safetyBlocked
? "blocked"
: executionFailed
? "failed"
: submitted
? "submitted"
: "not_submitted";
const deliveryLabel = safetyBlocked
? "安全阻止"
: executionFailed
? "执行失败"
: submitted
? "下发"
: "未下发";
const deliveryDetail = /no fresh valid 0x41a shade position/i.test(failureMessage)
? "未收到遮阳帘 CAN1 / 0x41A 实时位置,无法判断移动方向,系统已阻止盲动"
: failureMessage;
const transportLabel = capability === "infrared.emit"
? "红外 TCP"
: result.control_transport === "wireless_mqtt" ? "无线 CAN" : "有线 CAN";
return {
deliveryState: submitted ? "submitted" : "not_submitted",
deliveryLabel: submitted ? "下发" : "未下发",
deliveryState,
deliveryLabel,
deliveryTransport: transportLabel,
...(deliveryDetail ? { deliveryDetail } : {}),
};
};
const commands = [];
@@ -816,7 +871,7 @@
<span>
<b class="v2-can-command-channel">${escapeHtml(item.channel)}</b>
${item.parseSource ? `<b class="v2-can-command-source" data-parse-source="${escapeHtml(item.parseSource)}">${parsedCommandSourceLabel(item.parseSource)}</b>` : ""}
<b class="v2-can-command-delivery" data-delivery-state="${escapeHtml(item.deliveryState)}" title="${escapeHtml(`${item.deliveryTransport} · ${item.deliveryLabel}`)}">${escapeHtml(item.deliveryLabel)}</b>
<b class="v2-can-command-delivery" data-delivery-state="${escapeHtml(item.deliveryState)}" title="${escapeHtml([item.deliveryTransport, item.deliveryLabel, item.deliveryDetail].filter(Boolean).join(" · "))}">${escapeHtml(item.deliveryLabel)}</b>
<i class="v2-can-command-category">${escapeHtml(item.category)}</i>
</span>
<div class="v2-can-command-layer v2-can-command-semantic">
@@ -1083,20 +1138,16 @@
return operation;
}
function buildModuleSwitcher(mainPanel) {
function buildModuleSwitcher(mainPanel, modeRail) {
const switcher = make("nav", "v2-module-switcher", "");
switcher.id = "v2ModuleSwitcher";
switcher.dataset.unifiedSync = "true";
switcher.setAttribute("aria-label", "智能座舱远程控制与同步状态");
switcher.setAttribute("aria-label", "页面板块展开与收起");
switcher.innerHTML = `
<div class="v2-unified-brand">
<span class="v2-unified-brand-mark"><i data-lucide="waypoints"></i></span>
<div><h1>511工程-智能座舱</h1></div>
</div>
<div class="v2-remote-control-state"><i data-lucide="radio-tower"></i><span>网页控制链路</span><strong>已启用</strong></div>
<div class="v2-unified-live"><i></i><span>LIVE</span><b id="v2SyncClock">--:--:--</b></div>
<nav class="v2-panel-control-dock" id="v2PanelControlDock" aria-label="页面板块展开与收起"></nav>`;
mainPanel.prepend(switcher);
const modeRailBar = $(".mode-rail-bar", modeRail);
if (modeRailBar) modeRailBar.prepend(switcher);
else mainPanel.prepend(switcher);
const panelDock = $("#v2PanelControlDock", switcher);
const sidebarToggle = $("#v2SidebarToggle");
if (panelDock && sidebarToggle) panelDock.append(sidebarToggle);
@@ -1231,6 +1282,11 @@
return text;
}
function ambientPowerState() {
const value = $('[data-domain-panel="ambient_light"]')?.dataset.ambientPower;
return value === "on" || value === "off" ? value : "unknown";
}
function selectedTransportHasConnection() {
const selected = $('.vehicle-transport-options [data-control-transport][aria-pressed="true"]')
|| $('.vehicle-transport-options [data-control-transport].active');
@@ -1340,15 +1396,15 @@
rows: () => {
const [frontBrightness, rearBrightness] = splitReadout("#ambientBrightness");
const [frontColor, rearColor] = splitReadout("#ambientColorIndex");
const rawPower = normalizeDeviceReadout(readText("#ambientPower"));
const power = rawPower === "-" ? "-" : /OFF|关闭/.test(rawPower) ? "关闭" : "开启";
const powerState = ambientPowerState();
const power = powerState === "on" ? "开启" : powerState === "off" ? "关闭" : "-";
return [["开关状态", power], ["前排亮度", frontBrightness], ["前排颜色", frontColor], ["后排亮度", rearBrightness], ["后排颜色", rearColor]];
},
controls: () => {
const [frontBrightness, rearBrightness] = splitReadout("#ambientBrightness");
const [frontColor, rearColor] = splitReadout("#ambientColorIndex");
return [
selectControl("power", "开关状态", /OFF|关闭/.test(readText("#ambientPower")) ? "off" : "on", toggleOptions),
selectControl("power", "开关状态", ambientPowerState() === "off" ? "off" : "on", toggleOptions),
rangeControl("brightness", "全车亮度", frontBrightness === "-" ? rearBrightness : frontBrightness, 1, 5, 1, " 级"),
selectControl("color", "全车颜色", String(clampControlNumber(frontColor === "-" ? rearColor : frontColor, 1, 127, 1)), colorIndexOptions),
];
@@ -2141,9 +2197,7 @@
const sidebar = $(".v2-sidebar");
const sidebarFooter = $(".v2-sidebar-footer", sidebar);
const syncSidebar = make("section", "v2-sync-sidebar", `
<div class="v2-sync-sidebar-overview">
<div class="v2-sync-sidebar-heading"><h2>511工程-智能座舱</h2></div>
</div>
<div class="v2-sync-sidebar-overview"></div>
<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>
<ol id="v2SyncLogList" aria-live="polite"></ol>
@@ -2679,7 +2733,7 @@
requestHeader,
gestureDrawer,
);
buildModuleSwitcher(mainPanel);
buildModuleSwitcher(mainPanel, modeRail);
buildOutput(mainPanel, modeRail);
buildSyncControlLayer();
bindEvents();
+9 -6
View File
@@ -10,23 +10,26 @@
<link rel="icon" href="/icons/app-icon-192.png" />
<title>511工程-智能座舱</title>
<link rel="stylesheet" href="/styles.css?v=dual-seat-23" />
<link rel="stylesheet" href="/console-v2.css?v=delivery-state-68" />
<link rel="stylesheet" href="/console-v2.css?v=delivery-state-69" />
<link rel="stylesheet" href="/self-extension.css?v=gscu-tcp-43" />
<link rel="stylesheet" href="/speech-input.css?v=sensevoice-7" />
<link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-26" />
<link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-28" />
<link rel="stylesheet" href="/energy-monitor.css?v=energy-monitor-4" />
<link rel="stylesheet" href="/ui-consistency.css?v=spatial-ui-4" />
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
<script src="/vendor/vue/3.5.21/vue.runtime.global.prod.js" defer></script>
<script src="/vue-ui.js?v=vue-presentation-3" 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/FBXLoader.js" defer></script>
<script src="/network.js?v=android-pad-2" defer></script>
<script src="/app.js?v=dual-seat-131" defer></script>
<script src="/app.js?v=dual-seat-135" defer></script>
<script src="/speech-input.js?v=sensevoice-13" defer></script>
<script type="module" src="/gesture-control.js?v=android-pad-9"></script>
<script src="/pwa.js?v=android-pad-3" defer></script>
<script src="/console-v2.js?v=local-first-66" defer></script>
<script src="/self-extension.js?v=gscu-tcp-43" defer></script>
<script src="/assistant-chat.js?v=codex-assistant-27" defer></script>
<script src="/console-v2.js?v=local-first-70" defer></script>
<script src="/self-extension.js?v=gscu-tcp-44" defer></script>
<script src="/assistant-chat.js?v=codex-assistant-32" defer></script>
<script src="/energy-monitor.js?v=energy-monitor-4" defer></script>
</head>
<body class="console-v2">
+2 -1
View File
@@ -666,7 +666,8 @@
<span class="v2-extension-pet-mouth-closed" aria-hidden="true"></span>
</button>
</aside>`;
moduleSwitcher.insertAdjacentElement("afterend", root);
const homepageHeader = moduleSwitcher.closest(".v2-mode-rail") || moduleSwitcher;
homepageHeader.insertAdjacentElement("afterend", root);
buildDialogs();
return true;
+10 -7
View File
@@ -1,4 +1,4 @@
const CACHE_VERSION = "cognitive-agent-pad-v282";
const CACHE_VERSION = "cognitive-agent-pad-v290";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [
@@ -6,26 +6,29 @@ const APP_SHELL = [
"/index.html",
"/styles.css?v=dual-seat-23",
"/network.js?v=android-pad-2",
"/app.js?v=dual-seat-131",
"/app.js?v=dual-seat-135",
"/gesture-control.js?v=android-pad-9",
"/pwa.js?v=android-pad-3",
"/console-v2.css?v=delivery-state-68",
"/console-v2.js?v=local-first-66",
"/console-v2.css?v=delivery-state-69",
"/console-v2.js?v=local-first-70",
"/self-extension.css?v=gscu-tcp-43",
"/self-extension.js?v=gscu-tcp-43",
"/self-extension.js?v=gscu-tcp-44",
"/speech-input.css?v=sensevoice-7",
"/speech-input.js?v=sensevoice-13",
"/speech-audio-worklet.js?v=sensevoice-5",
"/assistant-chat.css?v=codex-assistant-26",
"/assistant-chat.js?v=codex-assistant-27",
"/assistant-chat.css?v=codex-assistant-28",
"/assistant-chat.js?v=codex-assistant-32",
"/energy-monitor.css?v=energy-monitor-4",
"/energy-monitor.js?v=energy-monitor-4",
"/ui-consistency.css?v=spatial-ui-4",
"/vue-ui.js?v=vue-presentation-3",
"/assets/pets/self-extension-cat.png?v=3",
"/assets/pets/self-extension-cat-entrance.png?v=3",
"/manifest.webmanifest",
"/icons/app-icon-192.png",
"/icons/app-icon-512.png",
"/vendor/lucide/0.468.0/lucide.min.js",
"/vendor/vue/3.5.21/vue.runtime.global.prod.js",
"/vendor/three/0.147.0/three.min.js",
"/vendor/three/0.147.0/fflate.min.js",
"/vendor/three/0.147.0/FBXLoader.js",
+275
View File
@@ -0,0 +1,275 @@
/* Shared spatial UI contract --------------------------------------------
Keep geometry consistent across legacy modules without changing their
domain-specific colors, content density, or interaction behavior.
Hierarchy: control 10px -> item 12px -> section 16px -> panel 18px ->
modal 22px. All modules consume the same semantic color tokens. */
:root {
--ui-control-height-compact: 30px;
--ui-control-height: 38px;
--ui-control-size-icon: 36px;
--ui-radius-control-inner: calc(var(--radius) - 2px);
--ui-radius-control: var(--radius);
--ui-radius-item: calc(var(--radius) + 2px);
--ui-radius-section: calc(var(--radius) + 6px);
--ui-radius-panel: calc(var(--radius) + 8px);
--ui-radius-modal: calc(var(--radius) + 12px);
--ui-space-card: 10px;
--ui-border-subtle: 1px solid var(--border);
--ui-shadow-item: 0 5px 14px color-mix(in oklch, var(--foreground) 6%, transparent);
--ui-shadow-panel:
0 14px 32px color-mix(in oklch, var(--foreground) 8%, transparent),
inset 0 1px 0 color-mix(in oklch, var(--card) 78%, transparent);
--ui-shadow-modal:
0 28px 72px color-mix(in oklch, var(--foreground) 22%, transparent),
0 4px 18px color-mix(in oklch, var(--primary) 8%, transparent),
inset 0 1px 0 color-mix(in oklch, var(--card) 84%, transparent);
--ui-focus-ring: 0 0 0 3px color-mix(in oklch, var(--ring) 22%, transparent);
}
/* Top-level workspace surfaces share one frame. */
.console-v2.v2-passive-display.v2-interactive-sync :is(
.v2-operation-shell,
.joint-twin-shell > .twin-band,
#v2DevicePanel,
#v2ExtensionPanel .v2-extension-sync-mount
) {
border: var(--ui-border-subtle);
border-radius: var(--ui-radius-panel);
box-shadow: var(--ui-shadow-panel);
}
/* The operation shell owns the frame; its active view must not draw a
second border directly inside the same spatial boundary. */
.console-v2.v2-passive-display.v2-interactive-sync
.v2-operation-shell > .v2-natural-panel {
border: 0;
border-radius: inherit;
box-shadow: none;
}
/* Modal surfaces and their backdrops use one elevation level. */
:is(
.mode-gallery-backdrop .mode-gallery,
.mode-editor-backdrop .mode-editor,
.energy-monitor-layer .energy-monitor-page
) {
border: 1px solid color-mix(in oklch, var(--primary) 18%, var(--border));
border-radius: var(--ui-radius-modal);
box-shadow: var(--ui-shadow-modal);
}
.mode-gallery-backdrop,
.mode-editor-backdrop,
.energy-monitor-layer {
background: color-mix(in oklch, var(--foreground) 42%, transparent);
-webkit-backdrop-filter: blur(14px);
backdrop-filter: blur(14px);
}
/* Section containers sit between panels and individual cards. */
.v2-sync-log,
.mode-gallery-group,
.energy-trend-card,
.energy-devices-card {
border-radius: var(--ui-radius-section);
}
/* Repeated content cards share radius and elevation while retaining their
own semantic state colors and specialized internal layouts. */
.v2-synced-device,
.v2-extension-gateway,
.v2-nfc-inline,
.mode-gallery-card,
.mode-editor-flow,
.mode-editor-command-card,
.energy-kpi {
border-radius: var(--ui-radius-item);
box-shadow: var(--ui-shadow-item);
}
.energy-kpi-primary {
box-shadow: 0 8px 20px color-mix(in oklch, var(--primary) 10%, transparent);
}
.v2-extension-gateway,
.v2-nfc-inline {
padding: var(--ui-space-card);
}
/* Sidebar cards and their nested segmented controls preserve a clear
parent/child radius relationship. */
.console-v2 :is(
.v2-sync-transport-field,
.v2-sync-execution-field,
.v2-can-command-panel
) {
border-radius: var(--ui-radius-item);
}
/* The compact-sidebar rules in console-v2.css intentionally use highly
specific selectors. Keep all peer cards on the shared item radius even
when that layout is active. */
.console-v2.v2-passive-display.v2-interactive-sync :is(
.v2-sync-transport-field,
.v2-sync-execution-field,
.v2-can-command-panel
) {
border-radius: var(--ui-radius-item) !important;
}
.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-log {
border-radius: var(--ui-radius-section) !important;
}
.console-v2 :is(.segmented, .twin-selector) {
gap: 4px;
padding: 3px;
background: var(--muted);
border: var(--ui-border-subtle);
border-radius: var(--ui-radius-control);
}
.console-v2 .segmented > :is(.segment, .mode-option),
.console-v2 .twin-selector > .twin-seat-button {
border-radius: var(--ui-radius-control-inner);
}
.console-v2 .segmented > .mode-option {
min-height: var(--ui-control-height);
height: var(--ui-control-height);
gap: 5px;
}
.console-v2 .twin-selector > .twin-seat-button {
height: 32px;
}
/* Primary header controls use one medium size. */
.console-v2 #v2PanelControlDock > .v2-panel-control-button,
.console-v2.v2-passive-display.v2-interactive-sync
.v2-mode-rail .mode-rail-actions > :is(
#modeGalleryToggle,
#saveModeToggle,
#modeRailToggle
) {
min-height: var(--ui-control-height);
height: var(--ui-control-height);
border-radius: var(--ui-radius-control);
}
.console-v2.v2-passive-display.v2-interactive-sync
#v2PanelControlDock > .v2-panel-control-button,
.console-v2.v2-passive-display.v2-interactive-sync
#v2PanelControlDock > #v2SidebarToggle.v2-panel-control-button {
border-radius: var(--ui-radius-control) !important;
}
.console-v2.v2-passive-display.v2-interactive-sync
.v2-mode-rail #modeRailToggle {
width: var(--ui-control-height);
min-width: var(--ui-control-height);
}
/* Repeated inline actions use one compact size. */
.console-v2 :is(
.v2-device-adjust-toggle,
.v2-extension-nfc-launch,
.v2-extension-gateway > button,
.v2-nfc-inline button
),
.energy-trend-tabs button {
min-height: var(--ui-control-height-compact);
height: var(--ui-control-height-compact);
border-radius: var(--ui-radius-control-inner);
}
/* Header icon actions are distinct from circular record/send actions. */
.assistant-drawer-actions .context-reset,
.assistant-drawer-close,
.mode-gallery-close,
.mode-editor-header > button,
#energyMonitorClose {
width: var(--ui-control-size-icon);
min-width: var(--ui-control-size-icon);
height: var(--ui-control-size-icon);
min-height: var(--ui-control-size-icon);
border-radius: var(--ui-radius-control);
}
/* Text fields align with medium controls and use the same focus treatment. */
.mode-editor-field input,
.mode-editor-field textarea {
border-radius: var(--ui-radius-control);
}
/* Reserve the right side for the microphone/send actions. Anchoring the hint
from the content edge prevents the legacy right offsets from pushing it
outside narrow assistant composers. */
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync
.v2-request-form .composer-hint {
left: 16px !important;
right: auto !important;
bottom: 8px;
max-width: calc(100% - 132px);
}
/* Delivery states use the same semantic feedback language in the sidebar and
in the compact assistant command card. */
.console-v2.v2-passive-display.v2-interactive-sync
.v2-can-command-delivery:is(
[data-delivery-state="blocked"],
[data-delivery-state="failed"]
) {
color: var(--destructive);
background: color-mix(in oklch, var(--destructive) 10%, var(--card));
border-color: color-mix(in oklch, var(--destructive) 28%, var(--border));
}
.assistant-dispatched-controls-list > li:is(
[data-delivery-state="blocked"],
[data-delivery-state="failed"]
) {
background: color-mix(in oklch, var(--destructive) 4%, var(--card));
}
.assistant-dispatched-controls-list > li:is(
[data-delivery-state="blocked"],
[data-delivery-state="failed"]
) .assistant-dispatched-control-icon {
color: var(--destructive);
}
.console-v2 :is(
#v2PanelControlDock > .v2-panel-control-button,
.v2-mode-rail .mode-rail-actions > button,
.segmented > button,
.twin-selector > button,
.v2-device-adjust-toggle,
.v2-extension-nfc-launch,
.v2-extension-gateway > button,
.v2-nfc-inline button
):focus-visible,
.mode-gallery :is(button, input, textarea):focus-visible,
.mode-editor :is(button, input, textarea):focus-visible,
.energy-monitor-page button:focus-visible {
outline: none;
box-shadow: var(--ui-focus-ring);
}
@media (max-width: 900px) {
:root {
--ui-control-height: 40px;
--ui-control-height-compact: 32px;
--ui-control-size-icon: 38px;
}
}
@media (prefers-reduced-motion: reduce) {
.v2-synced-device,
.mode-gallery-card,
.energy-kpi,
.console-v2 button {
transition-duration: 0.01ms !important;
}
}
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018-present, Yuxi (Evan) You
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
File diff suppressed because one or more lines are too long
+176
View File
@@ -0,0 +1,176 @@
(function () {
"use strict";
const VUE_VERSION = "3.5.21";
const vue = window.Vue;
const unavailable = {
available: false,
version: VUE_VERSION,
renderDispatchedControls: () => false,
renderModeChips: () => false,
};
if (!vue?.createApp || !vue?.h || !vue?.reactive) {
window.CognitiveAgentVueUI = unavailable;
return;
}
const { Fragment, createApp, h, nextTick, reactive } = vue;
const modeMounts = new WeakMap();
const commandMounts = new WeakMap();
const text = (value, fallback = "") => String(value ?? fallback);
function refreshIcons() {
nextTick(() => window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }));
}
function replaceReactiveArray(target, values) {
target.splice(0, target.length, ...values.map((value) => ({ ...value })));
}
function renderModeEntry(entry) {
const key = text(entry.key || `${entry.type}-${entry.id || entry.label}`);
if (entry.type === "label") {
return h("span", {
key,
class: "mode-group-label",
"data-mode-group-label": text(entry.id),
}, text(entry.label));
}
if (entry.type === "divider") {
return h("span", {
key,
class: "mode-group-divider",
"data-mode-divider": text(entry.id),
"aria-hidden": "true",
});
}
const children = [
h("i", {
class: "mode-chip-icon",
"data-lucide": text(entry.icon, "bookmark"),
}),
h("span", { class: "mode-chip-label" }, text(entry.label, "模式")),
h("small", { class: "mode-chip-summary" }, text(entry.summary, "整车联动")),
];
if (entry.cycleLabel) {
children.push(h("small", { class: "mode-cycle-badge" }, text(entry.cycleLabel)));
}
children.push(h("i", {
class: "mode-chip-run",
"data-lucide": "play",
"data-mode-run": "true",
title: `执行${text(entry.label, "模式")}`,
}));
return h("button", {
key,
type: "button",
title: text(entry.title),
"data-mode-profile-id": text(entry.id),
"data-mode-command": text(entry.command),
"data-mode-origin": text(entry.origin, "preset"),
...(entry.behavior ? { "data-mode-behavior": text(entry.behavior) } : {}),
"aria-label": text(entry.ariaLabel, `${text(entry.label, "模式")},点击执行`),
"aria-pressed": "false",
}, children);
}
function createModeMount(container) {
const state = reactive({ entries: [] });
const app = createApp({
name: "N7ModeChipRail",
setup() {
return () => h(Fragment, null, state.entries.map(renderModeEntry));
},
});
app.mount(container);
const mount = { app, state };
modeMounts.set(container, mount);
return mount;
}
function renderModeChips(container, entries) {
if (!(container instanceof Element) || !Array.isArray(entries)) return false;
const mount = modeMounts.get(container) || createModeMount(container);
replaceReactiveArray(mount.state.entries, entries);
container.dataset.vueSurface = "mode-chips";
refreshIcons();
return true;
}
function renderCommandRow(command, index) {
const semantic = text(command.semantic);
const actual = text(command.command);
const accessibleText = [
semantic ? `语义控制指令:${semantic}` : "",
actual ? `实际控制指令:${actual}` : "",
command.deliveryLabel
? `下发状态:${text(command.deliveryLabel)}${command.deliveryDetail ? `(${text(command.deliveryDetail)})` : ""}`
: "",
].filter(Boolean).join(";");
return h("li", {
key: text(command.key, `command-${index}`),
title: accessibleText,
"data-delivery-state": text(command.deliveryState, "not_submitted"),
}, [
h("span", { class: "assistant-dispatched-control-icon" }, [
h("i", { "data-lucide": text(command.icon, "binary") }),
]),
h("span", { class: "assistant-dispatched-control-label" }, text(command.category, "控制指令")),
h("strong", { class: "assistant-dispatched-control-value" }, text(command.value, "控制")),
h("small", { class: "visually-hidden" }, accessibleText),
]);
}
function createCommandMount(container) {
const state = reactive({ commands: [], open: true });
const app = createApp({
name: "N7DispatchedControlsCard",
setup() {
return () => h("details", {
class: "assistant-dispatched-controls",
open: state.open,
onToggle(event) {
state.open = event.currentTarget.open;
},
}, [
h("summary", null, [
h("span", { class: "assistant-dispatched-controls-heading-icon" }, [
h("i", { "data-lucide": "list-tree" }),
]),
h("b", null, "下发控制指令"),
h("strong", { class: "assistant-dispatched-controls-count" }, String(state.commands.length)),
h("i", {
class: "assistant-dispatched-controls-chevron",
"data-lucide": "chevron-down",
}),
]),
h("ol", { class: "assistant-dispatched-controls-list" }, state.commands.map(renderCommandRow)),
]);
},
});
app.mount(container);
const mount = { app, state };
commandMounts.set(container, mount);
return mount;
}
function renderDispatchedControls(container, commands) {
if (!(container instanceof Element) || !Array.isArray(commands) || !commands.length) {
return false;
}
const mount = commandMounts.get(container) || createCommandMount(container);
replaceReactiveArray(mount.state.commands, commands);
container.dataset.vueSurface = "dispatched-controls";
refreshIcons();
return true;
}
document.documentElement.classList.add("vue-presentation-enabled");
document.documentElement.dataset.vueVersion = VUE_VERSION;
window.CognitiveAgentVueUI = Object.freeze({
available: true,
version: VUE_VERSION,
renderDispatchedControls,
renderModeChips,
});
}());