Files
Cognitive_Agent/apps/web-console/console-v2.js
T

2252 lines
103 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
"use strict";
const pageDefinitions = [
{ index: 0, label: "整车联合数字孪生", shortLabel: "数字孪生", target: ".joint-twin-shell" },
{ index: 1, label: "决策轨迹与校验", shortLabel: "决策校验", target: "#panel-trace" },
{ index: 2, label: "指令配置与硬件映射", shortLabel: "配置映射", target: "#panel-config" },
{ index: 3, label: "CAN 编译计划", shortLabel: "CAN 计划", target: "#panel-plan" },
{ index: 4, label: "执行结果与硬件回读", shortLabel: "执行回读", target: "#panel-result" },
];
const $ = (selector, root = document) => root.querySelector(selector);
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
const ui = {
controlMode: sessionStorage.getItem("cognitive-agent.control-mode") === "gesture"
? "gesture"
: "natural",
activeModule: "self-orchestration",
activePage: 0,
sidebarCollapsed: true,
};
let reasoningPickerControl = null;
let validatedCommandsVisible = false;
let validatedCommandContext = { requestId: "", utterance: "" };
function resetValidatedCommandContext() {
validatedCommandsVisible = false;
validatedCommandContext = { requestId: "", utterance: "" };
}
function rememberValidatedCommandContext(result = {}) {
validatedCommandContext = {
requestId: String(result.request_id || ""),
utterance: String(result.utterance || "").trim(),
};
}
function validatedCommandsBelongToTask(task = {}) {
if (!validatedCommandsVisible) return false;
const result = task.result && typeof task.result === "object" ? task.result : {};
const requestId = String(result.request_id || "");
const utterance = String(task.utterance || result.utterance || "").trim();
if (validatedCommandContext.requestId && requestId
&& validatedCommandContext.requestId !== requestId) return false;
if (validatedCommandContext.utterance && utterance
&& validatedCommandContext.utterance !== utterance
&& !validatedCommandContext.utterance.includes(utterance)
&& !utterance.includes(validatedCommandContext.utterance)) return false;
return true;
}
function make(tag, className, html) {
const node = document.createElement(tag);
if (className) node.className = className;
if (html != null) node.innerHTML = html;
return node;
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
const normalizedCanTarget = (target = {}) => String(
target.zone || target.instance || target.device || "vehicle",
).replaceAll("_", "-");
const normalizedOnOff = (value) => {
if ([true, 1, "1", "on", "open", "enabled"].includes(value)) return "on";
if ([false, 0, "0", "off", "close", "disabled"].includes(value)) return "off";
return null;
};
const boundedInteger = (value, minimum, maximum) => {
const numeric = Number(value);
return Number.isInteger(numeric) && numeric >= minimum && numeric <= maximum
? numeric
: null;
};
function parsedCanCommandForOperation(operation = {}) {
const capability = String(operation.capability || "");
const parameters = operation.parameters || {};
const target = normalizedCanTarget(operation.target);
const onOff = normalizedOnOff(parameters.state ?? parameters.power);
const hyphenated = (value) => String(value || "").replaceAll("_", "-");
const parseSource = String(operation.parse_source || "");
const command = (category, text, channel = "CAN1", frame = "") => ({
channel,
category,
command: text,
...(frame ? { frame } : {}),
...(parseSource ? { parseSource } : {}),
});
const hexByte = (value) => Number(value).toString(16).padStart(2, "0").toUpperCase();
const littleEndian16 = (value) => {
const numeric = Number(value) & 0xFFFF;
return `${hexByte(numeric & 0xFF)} ${hexByte((numeric >>> 8) & 0xFF)}`;
};
const nativeCan0Frame = (opcode, motor, direction, paramA, paramB) => {
const control = (Number(motor) & 0x03) | ((Number(direction) & 0x03) << 2) | 0x10;
return `0x300 · ${hexByte(opcode)} ${hexByte(control)} <SEQ_LO> <SEQ_HI> ${littleEndian16(paramA)} ${littleEndian16(paramB)}`;
};
if (capability === "infrared.emit") {
const slot = boundedInteger(parameters.slot, 1, 248);
const action = String(parameters.action || "红外动作").trim();
const deviceName = String(operation.target?.name || operation.target?.device || "红外外设").trim();
if (slot !== null) {
return {
channel: "IR",
category: `${deviceName} · ${action}`,
command: `infrared emit ${slot}`,
frame: JSON.stringify({ action: "emit", data: { no: slot }, type: "infrared" }),
frameLabel: "GSCU1B TCP",
...(parseSource ? { parseSource } : {}),
};
}
}
const isPassengerSeat = new Set(["passenger", "front-right"]).has(target);
if (isPassengerSeat) {
const passengerAxes = {
"seat.set_longitudinal_position": ["副驾座椅前后调节", "horizontal", 0],
"seat.set_cushion_tilt": ["副驾座椅高度调节", "basin", 1],
"seat.set_backrest_recline": ["副驾座椅靠背调节", "backrest", 2],
"seat.set_legrest_height": ["副驾座椅腿托调节", "legrest", 3],
};
const axis = passengerAxes[capability];
const position = boundedInteger(parameters.position_pct, 0, 100);
if (axis && position !== null) {
const motion = operation.motion || {};
const delta = boundedInteger(motion.delta_position_pct, 1, 100);
const relativeDirections = {
horizontal: { increase: "backward", decrease: "forward" },
basin: { increase: "down", decrease: "up" },
backrest: { increase: "backward", decrease: "forward" },
legrest: { increase: "up", decrease: "down" },
};
const relativeDirection = relativeDirections[axis[1]]?.[motion.direction];
return command(
axis[0],
motion.mode === "relative" && relativeDirection && delta !== null
? `seat passenger ${axis[1]} relative ${relativeDirection} ${delta}`
: `seat passenger ${axis[1]} position ${position}`,
"CAN0",
position === 0
? nativeCan0Frame(0x0B, axis[2], 0, 0x4B52, 120)
: nativeCan0Frame(0x01, axis[2], 0, position * 100, 30000),
);
}
if (new Set(["seat.set_ventilation", "seat.set_heating"]).has(capability)) {
const feature = capability === "seat.set_heating" ? "heat" : "vent";
const level = boundedInteger(parameters.level, 1, 3);
const setting = onOff === "off"
? "off"
: ({ 1: "low", 2: "medium", 3: "high" })[level];
if (setting) {
return command(
"副驾座椅通风/加热",
`seat passenger ${feature} ${setting}`,
"CAN0",
);
}
}
if (capability === "seat.start_dynamic_mode") {
const mode = boundedInteger(parameters.mode_id, 1, 2);
if (mode !== null) {
const dynamic = mode === 1 ? "rocking-horse" : "cradle";
return command(
"副驾座椅动态模式",
`seat passenger dynamic ${dynamic}`,
"CAN0",
nativeCan0Frame(0x0E, 0, 0, mode, 0),
);
}
}
if (capability === "seat.stop_dynamic_mode") {
return command(
"副驾座椅动态模式",
"seat passenger dynamic stop",
"CAN0",
nativeCan0Frame(0x0F, 0, 0, 0, 0),
);
}
}
if (capability === "door.set_rear_lock") {
const lockState = String(parameters.state || "");
const action = new Set(["lock", "unlock"]).has(lockState)
? lockState
: onOff === "on" ? "lock" : onOff === "off" ? "unlock" : null;
if (action) return command("后排门锁", `door rear ${action}`);
}
if (capability === "door.set_vehicle_lock") {
const lockState = String(parameters.state || "");
if (new Set(["lock", "unlock"]).has(lockState)) {
return command("整车门锁", `door vehicle ${lockState}`);
}
}
if (capability === "window.set_motion") {
const scope = new Set(["all", "front", "rear", "front-left", "front-right", "rear-left", "rear-right"]);
const motion = String(parameters.motion || parameters.action || "");
if (scope.has(target) && new Set(["open", "close", "stop"]).has(motion)) {
return command("车窗动作", `window ${target} ${motion}`);
}
}
if (capability === "window.set_position") {
const position = boundedInteger(parameters.position_pct, 0, 100);
if (new Set(["front-left", "front-right", "rear-left", "rear-right"]).has(target)
&& position !== null) {
return command("单窗位置", `window ${target} position ${position}`);
}
}
if (capability === "climate.set_power" && onOff) {
return command("空调电源", `ac ${onOff}`);
}
if (capability === "climate.set_fan_level") {
const level = boundedInteger(parameters.level ?? parameters.fan_level, 1, 7);
if (level !== null) return command("空调风量", `ac fan ${level}`);
}
if (capability === "climate.set_outlet_mode") {
const mode = hyphenated(parameters.mode ?? parameters.outlet_mode);
const modes = new Set(["face", "face-feet", "feet", "feet-defrost", "defrost", "face-feet-defrost", "face-defrost"]);
if (modes.has(mode)) return command("出风模式", `ac mode ${mode}`);
}
if (capability === "climate.set_temperature") {
const temperature = boundedInteger(parameters.temperature_c, 18, 32);
if (new Set(["driver", "passenger", "rear"]).has(target) && temperature !== null) {
return command("空调温度", `ac temp ${target} ${temperature}`);
}
}
if (capability === "climate.set_control_mode") {
const mode = String(parameters.mode ?? parameters.control_mode ?? "");
if (new Set(["manual", "auto"]).has(mode)) return command("空调控制方式", `ac control ${mode}`);
}
if (capability === "climate.set_cycle_mode") {
const mode = String(parameters.mode ?? parameters.cycle_mode ?? "");
if (new Set(["internal", "external"]).has(mode)) return command("空调循环", `ac cycle ${mode}`);
}
if (capability === "outlet.set_switch_state") {
const state = String(parameters.state ?? "");
if (new Set(["driver-left", "driver-right", "passenger-left", "passenger-right"]).has(target)
&& new Set(["open", "close"]).has(state)) {
return command("单风口开关", `outlet ${target} ${state}`);
}
}
if (capability === "outlet.set_position_percent") {
const horizontal = boundedInteger(parameters.horizontal_pct, 0, 100);
const vertical = boundedInteger(parameters.vertical_pct, 0, 100);
if (new Set(["driver-left", "driver-right", "passenger-left", "passenger-right"]).has(target)
&& horizontal !== null && vertical !== null) {
return command("风口百分比", `outlet ${target} percent ${horizontal} ${vertical}`);
}
}
if (capability === "outlet.set_calibrated_position") {
const horizontal = boundedInteger(parameters.horizontal_value, -128, 126);
const vertical = boundedInteger(parameters.vertical_value, -128, 126);
if (new Set(["driver-left", "driver-right", "passenger-left", "passenger-right"]).has(target)
&& horizontal !== null && vertical !== null) {
return command("风口标定坐标", `outlet ${target} position ${horizontal} ${vertical}`);
}
}
if (new Set(["seat.set_driver_ventilation_native", "seat.set_driver_heating_native"]).has(capability)
&& new Set(["driver", "front-left"]).has(target)) {
const feature = capability.includes("heating") ? "heat" : "vent";
const level = boundedInteger(parameters.level, 1, 2);
const setting = onOff === "off" ? "off" : level === 1 ? "high" : level === 2 ? "low" : null;
if (setting) return command("主驾座椅通风/加热", `seat driver ${feature} ${setting}`);
}
if (capability === "seat.set_driver_massage_native"
&& new Set(["driver", "front-left"]).has(target)) {
const action = String(parameters.action || "");
if (action === "off") return command("主驾座椅按摩", "seat driver massage off");
if (action === "strength") {
const strength = { 1: "weak", 2: "medium", 3: "strong" }[Number(parameters.strength)]
|| (new Set(["weak", "medium", "strong"]).has(parameters.strength) ? parameters.strength : null);
if (strength) return command("主驾座椅按摩", `seat driver massage strength ${strength}`);
}
if (action === "mode" && new Set(["wave", "pulse", "stretch", "slack", "relax"]).has(parameters.mode)) {
return command("主驾座椅按摩", `seat driver massage mode ${parameters.mode}`);
}
}
if (capability === "seat.set_driver_axis_native"
&& new Set(["driver", "front-left"]).has(target)) {
const axis = String(parameters.axis || "");
const direction = String(parameters.direction || "");
const validDirections = {
horizontal: new Set(["forward", "backward", "stop"]),
height: new Set(["up", "down", "stop"]),
backrest: new Set(["forward", "backward", "stop"]),
basin: new Set(["up", "down", "stop"]),
};
if (validDirections[axis]?.has(direction)) {
if (direction === "stop") return command("主驾座椅调节", `seat driver ${axis} stop`);
const duration = boundedInteger(parameters.duration_ms, 100, 2000);
if (duration !== null) return command("主驾座椅调节", `seat driver ${axis} ${direction} ${duration}`);
}
}
if (capability === "seat.set_driver_position_native") {
const position = boundedInteger(parameters.position ?? parameters.slot, 1, 3);
if (position !== null) return command("主驾座椅记忆", `seat driver position ${position}`);
}
if (capability === "seat.recall_driver_memory_native") {
const position = boundedInteger(parameters.position ?? parameters.slot, 1, 3);
if (position !== null) return command("主驾座椅记忆", `seat driver memory recall ${position}`);
}
if (capability === "interior_light.set_indoor_power" && onOff) {
return command("室内灯总开关", `light indoor ${onOff}`);
}
if (capability === "interior_light.set_reading_power" && target === "all" && onOff) {
return command("全部阅读灯", `light reading all ${onOff}`);
}
if (capability === "ambient_light.set_power" && onOff) {
return command("氛围灯", `light ambient ${onOff}`);
}
if (capability === "ambient_light.set_color_index") {
const color = boundedInteger(parameters.color_index, 1, 127);
if (color !== null) return command("氛围灯", `light ambient color ${color}`);
}
if (capability === "ambient_light.set_brightness_level") {
const level = boundedInteger(parameters.brightness_level, 1, 5);
if (level !== null) return command("氛围灯", `light ambient brightness ${level}`);
}
if (capability === "roof.set_shade_motion") {
const motion = String(parameters.motion || "");
if (new Set(["open", "close", "half", "stop"]).has(motion)) {
return command("遮阳帘", `shade ${motion}`);
}
}
if (capability === "roof.set_shade_position") {
const position = boundedInteger(parameters.position_pct ?? parameters.shade_position_pct, 0, 100);
if (position !== null) return command("遮阳帘", `shade position ${position}`);
}
return null;
}
function parsedCanCommands(result = {}) {
const configurations = result.function_configs || {};
const configurationEntries = Object.entries(configurations);
if (!configurationEntries.length && result.function_config) {
configurationEntries.push(["default", result.function_config]);
}
const executionResults = [
...(result.execution_report?.spec?.results || []),
...(result.operation_results || []),
...Object.values(result.phase_execution_reports || {}).flatMap(
(report) => report?.spec?.results || [],
),
];
const executionByRef = new Map();
executionResults.forEach((execution) => {
const ref = String(execution?.ref || "");
if (!ref) return;
const previous = executionByRef.get(ref);
if (!previous || (!previous.telemetry && execution?.telemetry)) {
executionByRef.set(ref, execution);
}
});
const submittedStates = new Set(["submitted", "queued", "published", "sent", "delivered"]);
const deliveryForOperation = (operation) => {
const capability = String(operation?.capability || "");
const operationId = String(operation?.id || "");
const execution = executionByRef.get(operationId) || executionResults.find((candidate) => {
const ref = String(candidate?.ref || "");
return ref && operationId && (
operationId.startsWith(`${ref}-`) || ref.startsWith(`${operationId}-`)
);
});
const telemetry = execution?.telemetry || {};
const mqttMessages = Array.isArray(telemetry.mqtt_messages) ? telemetry.mqtt_messages : [];
const deliveryStates = [
telemetry.delivery_status,
...mqttMessages.map((message) => message?.delivery_status),
].map((value) => String(value || "").toLowerCase());
const submitted = result.execution_mode === "hardware"
&& telemetry.simulated !== true
&& (
deliveryStates.some((value) => submittedStates.has(value))
|| (Array.isArray(telemetry.sent_frames) && telemetry.sent_frames.length > 0)
|| telemetry.gateway_acknowledged === true
|| telemetry.feedback_observed === true
|| telemetry.physical_effect_confirmed === true
);
const transportLabel = capability === "infrared.emit"
? "红外 TCP"
: result.control_transport === "wireless_mqtt" ? "无线 CAN" : "有线 CAN";
return {
deliveryState: submitted ? "submitted" : "not_submitted",
deliveryLabel: submitted ? "下发" : "未下发",
deliveryTransport: transportLabel,
};
};
const commands = [];
configurationEntries.forEach(([_configKey, configuration]) => {
(configuration?.spec?.operations || []).forEach((operation) => {
const item = parsedCanCommandForOperation(operation);
if (item) commands.push({ ...item, ...deliveryForOperation(operation) });
});
});
const unique = new Map();
commands.forEach((item) => {
const key = `${item.channel}:${item.command}`;
const previous = unique.get(key);
if (!previous
|| (previous.deliveryState !== "submitted" && item.deliveryState === "submitted")
|| (previous.parseSource !== "local_rule" && item.parseSource === "local_rule")) {
unique.set(key, item);
}
});
return [...unique.values()];
}
function parsedCommandSourceLabel(source) {
return {
direct_control: "设备控件",
saved_mode_command_flow: "模式卡片",
local_rule: "本地规则",
speech_semantic: "语音语义",
}[source] || "云端模型";
}
function renderParsedCanCommands(result, { message = "" } = {}) {
const panel = $("#v2CanCommandPanel");
const list = $("#v2CanCommandList");
const count = $("#v2CanCommandCount");
if (!panel || !list || !count) return;
const commands = result ? parsedCanCommands(result) : [];
count.textContent = String(commands.length);
panel.dataset.state = commands.length ? "ready" : message ? "pending" : "empty";
if (commands.length) {
list.innerHTML = `<ol>${commands.map((item) => `
<li data-can-channel="${escapeHtml(item.channel)}" data-delivery-state="${escapeHtml(item.deliveryState)}">
<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>
<i class="v2-can-command-category">${escapeHtml(item.category)}</i>
</span>
<code>${escapeHtml(item.command)}</code>
${item.frame ? `<small class="v2-can-command-frame">${escapeHtml(item.frameLabel || "固件原生 CAN0")} · ${escapeHtml(item.frame)}</small>` : ""}
</li>`).join("")}</ol>`;
} else {
list.innerHTML = `<p>${escapeHtml(message || "本次请求未解析出控制指令")}</p>`;
}
requestAnimationFrame(syncSidebarLogBaseline);
}
function renderDirectCanCommands(commands, deviceName) {
const panel = $("#v2CanCommandPanel");
const list = $("#v2CanCommandList");
const count = $("#v2CanCommandCount");
if (!panel || !list || !count) return;
const items = commands.map((command) => ({
channel: command.startsWith("seat passenger ") ? "CAN0" : "CAN1",
command,
deliveryState: "not_submitted",
deliveryLabel: "未下发",
}));
count.textContent = String(items.length);
panel.dataset.state = "ready";
list.innerHTML = `<ol>${items.map((item) => `
<li data-can-channel="${item.channel}" data-delivery-state="${item.deliveryState}">
<span>
<b class="v2-can-command-channel">${item.channel}</b>
<b class="v2-can-command-source" data-parse-source="direct_control">设备控件</b>
<b class="v2-can-command-delivery" data-delivery-state="${item.deliveryState}" title="尚未进入 CAN 传输链路">${item.deliveryLabel}</b>
<i class="v2-can-command-category">${escapeHtml(deviceName)}</i>
</span>
<code>${escapeHtml(item.command)}</code>
</li>`).join("")}</ol>`;
validatedCommandsVisible = true;
validatedCommandContext = {
requestId: "",
utterance: items.map((item) => item.command).join(";"),
};
requestAnimationFrame(syncSidebarLogBaseline);
}
function closeReasoningPicker() {
reasoningPickerControl?.close();
}
function installReasoningPicker(reasoningField, originalForm) {
const composer = $(".command-composer", originalForm);
if (!composer || !reasoningField) return;
reasoningField.classList.add("v2-reasoning-field");
reasoningField.hidden = true;
const trigger = make("button", "v2-reasoning-trigger", `
<i data-lucide="sparkles"></i>
<span>模型智能</span>
<strong id="v2ReasoningValue">均衡</strong>
<i class="v2-reasoning-caret" data-lucide="chevron-up"></i>`);
trigger.id = "v2ReasoningTrigger";
trigger.type = "button";
trigger.setAttribute("aria-haspopup", "listbox");
trigger.setAttribute("aria-expanded", "false");
trigger.setAttribute("aria-controls", "reasoningField");
trigger.setAttribute("aria-label", "选择模型智能程度,当前均衡");
$(".reasoning-segmented", reasoningField)?.setAttribute("role", "listbox");
const options = () => $$('[data-reasoning-effort]', reasoningField);
const place = () => {
if (reasoningField.hidden) return;
const triggerBox = trigger.getBoundingClientRect();
const width = Math.min(320, Math.max(250, window.innerWidth - 24));
const left = Math.min(
Math.max(12, triggerBox.left),
Math.max(12, window.innerWidth - width - 12),
);
reasoningField.style.width = `${width}px`;
reasoningField.style.left = `${left}px`;
const height = reasoningField.offsetHeight;
const above = triggerBox.top - height - 8;
const top = above >= 12
? above
: Math.min(triggerBox.bottom + 8, window.innerHeight - height - 12);
reasoningField.style.top = `${Math.max(12, top)}px`;
};
const sync = () => {
const selected = options().find((button) => button.classList.contains("active"));
const label = selected?.querySelector("strong")?.textContent?.trim() || "均衡";
const unavailable = options().length > 0 && options().every((button) => button.disabled);
$("#v2ReasoningValue", trigger).textContent = label;
trigger.classList.toggle("unavailable", unavailable);
trigger.setAttribute("aria-disabled", String(unavailable));
trigger.setAttribute("aria-label", `选择模型智能程度,当前${label}`);
options().forEach((button) => {
button.setAttribute("role", "option");
button.setAttribute("aria-selected", String(button === selected));
});
};
const close = () => {
reasoningField.hidden = true;
trigger.classList.remove("open");
trigger.setAttribute("aria-expanded", "false");
};
const open = () => {
reasoningField.hidden = false;
trigger.classList.add("open");
trigger.setAttribute("aria-expanded", "true");
place();
};
reasoningPickerControl = { close, open, place, sync, trigger };
const resetButton = $("#newConversation", originalForm);
const runButton = $("#runButton", originalForm);
if (resetButton) composer.append(resetButton);
composer.append(trigger);
if (runButton) composer.append(runButton);
document.body.append(reasoningField);
trigger.addEventListener("click", (event) => {
event.stopPropagation();
if (reasoningField.hidden) open();
else close();
});
reasoningField.addEventListener("click", (event) => {
event.stopPropagation();
if (!event.target.closest("[data-reasoning-effort]")) return;
requestAnimationFrame(() => {
sync();
close();
trigger.focus({ preventScroll: true });
});
});
new MutationObserver(sync).observe(reasoningField, {
attributes: true,
attributeFilter: ["class", "disabled"],
subtree: true,
});
window.addEventListener("resize", place);
sync();
}
function buildSidebar(commandPanel) {
commandPanel.classList.add("v2-sidebar");
commandPanel.id = "v2SidebarPanel";
commandPanel.innerHTML = "";
const sidebarToggle = make("button", "v2-sidebar-toggle v2-panel-control-button", '<i data-lucide="panel-left"></i><span class="visually-hidden">左侧控制台</span>');
sidebarToggle.id = "v2SidebarToggle";
sidebarToggle.type = "button";
sidebarToggle.setAttribute("aria-controls", commandPanel.id);
sidebarToggle.setAttribute("aria-label", "展开左侧控制台");
sidebarToggle.setAttribute("aria-pressed", "false");
sidebarToggle.setAttribute("aria-expanded", "false");
sidebarToggle.title = "展开左侧控制台";
const footer = make("footer", "v2-sidebar-footer", `
<span class="v2-live-dot"></span>
<div><strong>智舱控制链路</strong><small id="v2ConnectionText">正在连接服务</small></div>
<button type="button" id="v2LogoutButton" title="退出登录" aria-label="退出登录"><i data-lucide="log-out"></i></button>`);
commandPanel.append(sidebarToggle, footer);
}
function prepareRuntimeControls(originalForm) {
const vehicleField = $(".vehicle-field", originalForm);
const transportField = $("#vehicleTransportField", originalForm);
const providerField = $(".field-group:has(.segmented [data-provider])", originalForm);
const reasoningField = $("#reasoningField", originalForm);
const executionField = $(".field-group:has(.execution-mode-options)", originalForm);
const simulationFaultField = $("#simulationFaultField", originalForm);
transportField?.classList.add("v2-transport-field");
executionField?.classList.add("v2-execution-field");
simulationFaultField?.classList.add("v2-simulation-fault-field");
installReasoningPicker(reasoningField, originalForm);
[vehicleField, providerField].forEach((field) => {
if (!field) return;
field.classList.add("v2-runtime-data-field");
field.hidden = true;
});
}
function buildOperation(mainPanel, originalForm, panelHeading, requestHeader, gestureDrawer) {
const operation = make("section", "v2-operation-shell", `
<div class="v2-operation-glow" aria-hidden="true"></div>
<div class="v2-natural-panel" id="v2NaturalPanel"></div>
<div class="v2-gesture-panel" id="v2GesturePanel"></div>`);
mainPanel.prepend(operation);
const naturalPanel = $("#v2NaturalPanel");
if (panelHeading) {
panelHeading.classList.add("v2-operation-heading", "v2-module-heading");
const title = $("h2", panelHeading);
if (title) title.textContent = "AI助手";
naturalPanel.append(panelHeading);
}
const naturalGrid = make("div", "v2-natural-grid", "");
naturalGrid.id = "v2NaturalGrid";
originalForm.classList.add("v2-request-form");
naturalGrid.append(originalForm);
if (requestHeader) {
requestHeader.classList.add("v2-request-status");
naturalGrid.append(requestHeader);
}
naturalPanel.append(naturalGrid);
const gesturePanel = $("#v2GesturePanel");
const gestureHeading = make("header", "v2-operation-heading v2-module-heading v2-gesture-operation-heading", `
<h2>抬抬手,控制整车</h2>`);
gesturePanel.append(gestureHeading);
if (gestureDrawer) {
gestureDrawer.classList.add("v2-inline-gesture");
const cameraStage = $(".gesture-camera-stage", gestureDrawer);
const cameraToggle = $("#gestureStartButton", gestureDrawer);
const actionRail = $(".gesture-actions", gestureDrawer);
const statusStrip = $(".gesture-status-strip", gestureDrawer);
const gestureNow = $(".gesture-now", gestureDrawer);
const guideCard = $(".gesture-guide-card", gestureDrawer);
if (cameraToggle) {
cameraToggle.classList.add("v2-camera-toggle");
cameraToggle.setAttribute("aria-label", "开启摄像头");
cameraToggle.setAttribute("title", "开启摄像头");
}
if (actionRail && cameraToggle) {
actionRail.classList.add("v2-gesture-action-rail");
actionRail.append(cameraToggle);
}
const resultStack = make("div", "v2-gesture-result-stack", "");
resultStack.id = "v2GestureResultStack";
const recognitionCard = make("section", "v2-gesture-recognition-card", `
<span class="v2-gesture-result-label">手势识别结果</span>`);
const executionSlot = make("section", "v2-gesture-execution-slot", `
<span class="v2-gesture-result-label">执行结果</span>`);
executionSlot.id = "v2GestureExecutionSlot";
if (statusStrip) recognitionCard.append(statusStrip);
if (gestureNow) recognitionCard.append(gestureNow);
resultStack.append(recognitionCard, executionSlot);
if (cameraStage) gestureDrawer.append(cameraStage);
if (actionRail) gestureDrawer.append(actionRail);
gestureDrawer.append(resultStack);
if (guideCard) gestureDrawer.append(guideCard);
gesturePanel.append(gestureDrawer);
}
return operation;
}
function buildModuleSwitcher(mainPanel) {
const switcher = make("nav", "v2-module-switcher", "");
switcher.id = "v2ModuleSwitcher";
switcher.dataset.unifiedSync = "true";
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 panelDock = $("#v2PanelControlDock", switcher);
const sidebarToggle = $("#v2SidebarToggle");
if (panelDock && sidebarToggle) panelDock.append(sidebarToggle);
}
function buildOutput(mainPanel, modeRail) {
const output = make("section", "v2-output-shell", `
<header class="v2-output-heading">
<div>
<span class="v2-page-index" id="v2PageIndex">00</span>
<div><h2 id="v2PageTitle">整车联合数字孪生</h2></div>
</div>
<span class="v2-output-state"><i></i><span id="v2OutputState">LIVE STATE</span></span>
</header>
<div class="v2-output-content" id="v2OutputContent"></div>
<nav class="v2-page-rail" id="v2PageRail" aria-label="信息分页" hidden></nav>`);
mainPanel.append(output);
if (modeRail) {
modeRail.classList.add("v2-mode-rail");
mainPanel.insertBefore(modeRail, output);
}
const content = $("#v2OutputContent");
const twin = $(".joint-twin-shell", mainPanel);
const resultWorkspace = $(".result-workspace", mainPanel);
if (twin) content.append(twin);
if (resultWorkspace) content.append(resultWorkspace);
pageDefinitions.forEach((page) => {
const target = $(page.target, content);
if (!target) return;
target.dataset.v2Page = String(page.index);
target.classList.add("v2-output-page");
});
pageDefinitions.forEach((page) => {
const button = make("button", "v2-page-button", `
<span>${String(page.index).padStart(2, "0")}</span>
<i></i>
<strong>${page.shortLabel}</strong>`);
button.type = "button";
button.dataset.v2PageTarget = String(page.index);
button.setAttribute("aria-label", `${String(page.index).padStart(2, "0")} ${page.label}`);
$("#v2PageRail").append(button);
});
}
const displaySync = {
revision: -1,
requestId: null,
pollTimer: null,
deviceFrame: null,
deviceFingerprint: "",
activeDevice: null,
pinnedDevice: null,
previewClearTimer: null,
hoverSuppressedUntil: 0,
controlDevice: null,
deviceDrafts: {},
pendingControls: {},
logs: [],
logKeys: new Set(),
extensionFingerprint: "",
};
const readText = (selector, fallback = "--") => $(selector)?.textContent?.trim() || fallback;
const clampControlNumber = (value, min, max, fallback = min) => {
const numeric = Number.parseFloat(String(value ?? "").replace(/[^\d.-]/g, ""));
return Number.isFinite(numeric) ? Math.max(min, Math.min(max, numeric)) : fallback;
};
const splitReadout = (selector) => {
const [front = "-", rear = "-"] = readText(selector, "- / -").split("/");
return [front.trim() || "-", rear.trim() || "-"];
};
function seatFunctionValue(seat, feature) {
return $(`.seat-card[data-seat="${seat}"] [data-function="${feature}"] b`)?.textContent?.trim() || "--";
}
function seatAxisValue(seat, axis) {
return $(`.seat-card[data-seat="${seat}"] [data-axis="${axis}"] b`)?.textContent?.trim() || "--";
}
function seatLevelControlValue(seat, feature) {
const text = seatFunctionValue(seat, feature);
const level = text.match(/L(\d+)/i)?.[1];
return /OFF|关闭|--/.test(text) ? "off" : level === "2" ? "low" : "high";
}
function massageReadout() {
const text = seatFunctionValue("driver", "massage");
if (text === "--") return ["--", "--"];
if (/OFF|关闭/.test(text)) return ["关闭", "关闭"];
const [mode = "开启", strength = "默认"] = text.split("·").map((item) => item.trim());
return [mode, strength];
}
const levelOptions = [
["off", "关闭"], ["high", "1 档(高)"], ["low", "2 档(低)"],
];
const toggleOptions = [["off", "关闭"], ["on", "开启"]];
const massageModeOptions = [
["off", "关闭"], ["relax", "放松"], ["stretch", "伸展"], ["slack", "松弛"], ["wave", "波浪"], ["pulse", "脉冲"],
];
const massageStrengthOptions = [
["off", "关闭"], ["weak", "低"], ["medium", "中"], ["strong", "高"],
];
const colorIndexNames = ["皓月白", "冰川蓝", "松石绿", "星云紫", "琥珀橙", "玫瑰红"];
const colorIndexOptions = Array.from({ length: 127 }, (_item, index) => [
String(index + 1), colorIndexNames[index] ? `${index + 1} · ${colorIndexNames[index]}` : `色号 ${index + 1}`,
]);
const rangeControl = (key, label, value, min = 0, max = 100, step = 1, unit = "%", hint = "") => ({
key, label, type: "range", value: clampControlNumber(value, min, max), min, max, step, unit, hint,
});
const selectControl = (key, label, value, options) => ({ key, label, type: "select", value, options });
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) {
const draft = displaySync.deviceDrafts[deviceId];
return draft && Object.hasOwn(draft, control.key) ? draft[control.key] : control.value;
}
const syncedDevices = [
{
id: "passenger", name: "副驾座椅", icon: "armchair",
rows: () => [["前后", seatAxisValue("passenger", "longitudinal")], ["高度", seatAxisValue("passenger", "cushion_tilt")], ["靠背", seatAxisValue("passenger", "backrest_recline")], ["腿托", seatAxisValue("passenger", "legrest_height")]],
controls: () => [
rangeControl("longitudinal", "前后", seatAxisValue("passenger", "longitudinal")),
rangeControl("cushion_tilt", "高度", seatAxisValue("passenger", "cushion_tilt")),
rangeControl("backrest_recline", "靠背", seatAxisValue("passenger", "backrest_recline")),
rangeControl("legrest_height", "腿托", seatAxisValue("passenger", "legrest_height")),
],
commands: (values) => [
["longitudinal", "horizontal"],
["cushion_tilt", "basin"],
["backrest_recline", "backrest"],
["legrest_height", "legrest"],
].filter(([key]) => hasControlValue(values, key))
.map(([key, axis]) => `seat passenger ${axis} position ${Math.round(Number(values[key]))}`),
},
{
id: "driver", name: "主驾座椅", icon: "armchair",
rows: () => {
const [massageMode, massageStrength] = massageReadout();
return [["通风", seatFunctionValue("driver", "ventilation")], ["加热", seatFunctionValue("driver", "heating")], ["按摩模式", massageMode], ["按摩强度", massageStrength]];
},
controls: () => {
const [massageMode, massageStrength] = massageReadout();
return [
selectControl("ventilation", "通风", seatLevelControlValue("driver", "ventilation"), levelOptions),
selectControl("heating", "加热", seatLevelControlValue("driver", "heating"), levelOptions),
selectControl("massage_mode", "按摩模式", /关闭|--/.test(massageMode) ? "off" : massageMode.toLowerCase(), massageModeOptions),
selectControl("massage_strength", "按摩强度", /关闭|--/.test(massageStrength) ? "off" : massageStrength.toLowerCase(), massageStrengthOptions),
];
},
commands: (values) => {
const commands = [];
if (hasControlValue(values, "ventilation")) commands.push(`seat driver vent ${values.ventilation}`);
if (hasControlValue(values, "heating")) commands.push(`seat driver heat ${values.heating}`);
if (hasControlValue(values, "massage_mode")) {
commands.push(values.massage_mode === "off"
? "seat driver massage off"
: `seat driver massage mode ${values.massage_mode}`);
}
if (hasControlValue(values, "massage_strength")) {
commands.push(values.massage_strength === "off"
? "seat driver massage off"
: `seat driver massage strength ${values.massage_strength}`);
}
return [...new Set(commands)];
},
},
{
id: "windows", name: "车窗", icon: "gallery-vertical-end",
rows: () => [["左前窗", readText("#frontLeftWindow")], ["右前窗", readText("#frontRightWindow")], ["左后窗", readText("#rearLeftWindow")], ["右后窗", readText("#rearRightWindow")]],
controls: () => [
rangeControl("front_left", "左前窗", readText("#frontLeftWindow"), 0, 100, 1, "%", "0% 关闭 · 100% 打开"),
rangeControl("front_right", "右前窗", readText("#frontRightWindow")),
rangeControl("rear_left", "左后窗", readText("#rearLeftWindow")),
rangeControl("rear_right", "右后窗", readText("#rearRightWindow")),
],
commands: (values) => Object.entries({
front_left: "front-left", front_right: "front-right", rear_left: "rear-left", rear_right: "rear-right",
}).filter(([key]) => hasControlValue(values, key))
.map(([key, target]) => `window ${target} position ${Math.round(Number(values[key]))}`),
},
{
id: "shade", name: "遮阳帘", icon: "panels-top-left",
rows: () => [["当前位置", readText("#shadePositionValue")]],
controls: () => [rangeControl("position", "当前位置", readText("#shadePositionValue"), 0, 100, 1, "%", "0% 关闭 · 100% 打开")],
commands: (values) => hasControlValue(values, "position")
? [`shade position ${Math.round(Number(values.position))}`]
: [],
},
{
id: "reading", name: "阅读灯", icon: "book-open",
rows: () => [["开关状态", readText("#interiorLightPower", "等待反馈")]],
controls: () => [selectControl("power", "开关状态", /开启|ON/.test(readText("#interiorLightPower")) ? "on" : "off", toggleOptions)],
commands: (values) => hasControlValue(values, "power") ? [`light reading all ${values.power}`] : [],
},
{
id: "ambient", name: "氛围灯", icon: "sparkles",
rows: () => {
const [frontBrightness, rearBrightness] = splitReadout("#ambientBrightness");
const [frontColor, rearColor] = splitReadout("#ambientColorIndex");
const rawPower = normalizeDeviceReadout(readText("#ambientPower"));
const power = rawPower === "-" ? "-" : /OFF|关闭/.test(rawPower) ? "关闭" : "开启";
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),
rangeControl("brightness", "全车亮度", frontBrightness === "-" ? rearBrightness : frontBrightness, 1, 5, 1, " 级"),
selectControl("color", "全车颜色", String(clampControlNumber(frontColor === "-" ? rearColor : frontColor, 1, 127, 1)), colorIndexOptions),
];
},
commands: (values) => [
...(hasControlValue(values, "power") ? [`light ambient ${values.power}`] : []),
...(hasControlValue(values, "brightness") ? [`light ambient brightness ${Math.round(Number(values.brightness))}`] : []),
...(hasControlValue(values, "color") ? [`light ambient color ${Math.round(Number(values.color))}`] : []),
],
},
{
id: "headlight", name: "前大灯", icon: "car-front",
controllable: false,
rows: () => [["近光灯", readText("#lowBeamState")], ["远光灯", readText("#highBeamState")], ["雾灯", readText("#fogLightState")], ["转向灯", readText("#turnSignalState")]],
controls: () => [["low_beam", "近光灯", "#lowBeamState"], ["high_beam", "远光灯", "#highBeamState"], ["fog_light", "雾灯", "#fogLightState"], ["turn_signal", "转向灯", "#turnSignalState"]].map(([key, label, selector]) => selectControl(key, label, /开启|ON/.test(readText(selector)) ? "on" : "off", toggleOptions)),
},
{
id: "climate", name: "空调", icon: "fan",
rows: () => [["开关状态", readText("#climatePower", "等待反馈")], ["主驾温度", readText("#driverTemperature")], ["副驾温度", readText("#passengerTemperature")], ["风量", readText("#fanLevel")], ["循环模式", readText("#cycleMode")]],
controls: () => [
selectControl("power", "开关状态", /ON|ACTIVE|开启/.test(readText("#climatePower")) ? "on" : "off", toggleOptions),
rangeControl("driver_temperature", "主驾温度", Math.round(clampControlNumber(readText("#driverTemperature"), 18, 32, 24)), 18, 32, 1, "℃"),
rangeControl("passenger_temperature", "副驾温度", Math.round(clampControlNumber(readText("#passengerTemperature"), 18, 32, 24)), 18, 32, 1, "℃"),
rangeControl("fan_level", "风量", readText("#fanLevel"), 1, 7, 1, " 档"),
selectControl("cycle_mode", "循环模式", readText("#cycleMode").includes("内") ? "internal" : "external", [["internal", "内循环"], ["external", "外循环"]]),
],
commands: (values) => [
...(hasControlValue(values, "power") ? [`ac ${values.power}`] : []),
...(hasControlValue(values, "driver_temperature") ? [`ac temp driver ${Math.round(Number(values.driver_temperature))}`] : []),
...(hasControlValue(values, "passenger_temperature") ? [`ac temp passenger ${Math.round(Number(values.passenger_temperature))}`] : []),
...(hasControlValue(values, "fan_level") ? [`ac fan ${Math.round(Number(values.fan_level))}`] : []),
...(hasControlValue(values, "cycle_mode") ? [`ac cycle ${values.cycle_mode}`] : []),
],
},
];
function renderDeviceControl(device, control) {
const value = deviceControlValue(device.id, control);
if (control.type === "select") {
const options = control.options.map(([optionValue, label]) => `<option value="${escapeHtml(optionValue)}"${String(optionValue) === String(value) ? " selected" : ""}>${escapeHtml(label)}</option>`).join("");
return `<label class="v2-device-control-row v2-device-control-select"><span>${escapeHtml(control.label)}</span><select data-device-control="${device.id}" data-control-key="${control.key}" aria-label="${escapeHtml(device.name + control.label)}">${options}</select></label>`;
}
return `<label class="v2-device-control-row v2-device-control-range">
<span>${escapeHtml(control.label)}</span><output data-control-output="${control.key}">${escapeHtml(value)}${escapeHtml(control.unit)}</output>
<input type="range" min="${control.min}" max="${control.max}" step="${control.step}" value="${escapeHtml(value)}" data-device-control="${device.id}" data-control-key="${control.key}" data-control-unit="${escapeHtml(control.unit)}" aria-label="${escapeHtml(device.name + control.label)}" />
${control.hint ? `<small>${escapeHtml(control.hint)}</small>` : ""}
</label>`;
}
function renderSyncedDevices() {
const grid = $("#v2DeviceSyncGrid");
if (!grid) return;
grid.innerHTML = syncedDevices.map((device) => {
const rows = deviceReadoutRows(device);
const waiting = rows.some(([, value]) => value === "-");
const controllable = device.controllable !== false;
const controlOpen = controllable && displaySync.controlDevice === device.id;
const hasDraft = Boolean(displaySync.deviceDrafts[device.id]);
const pending = Boolean(displaySync.pendingControls[device.id]);
const stateLabel = pending ? "已发送 · 等待同步" : hasDraft ? "参数待发送" : `${rows.length} 项关键状态`;
return `<article class="v2-synced-device" data-device="${device.id}" data-ready="${String(!waiting)}" data-control-open="${String(controlOpen)}" data-has-draft="${String(hasDraft)}">
<header class="v2-device-card-header">
<button class="v2-device-card-focus" type="button" data-device-focus="${device.id}" aria-label="在孪生界面查看${device.name}">
<span><i data-lucide="${device.icon}"></i></span><span><strong>${device.name}</strong><small data-device-card-state>${stateLabel}</small></span><i class="v2-device-signal"></i>
</button>
<button class="v2-device-adjust-toggle" type="button" data-device-control-toggle="${device.id}" aria-expanded="${String(controlOpen)}"${controllable ? "" : " disabled"}><i data-lucide="${controllable ? "sliders-horizontal" : "eye"}"></i><span>${controllable ? (controlOpen ? "收起" : "调节") : "只读"}</span></button>
</header>
<dl>${rows.map(([label, value]) => `<div><dt>${escapeHtml(label)}</dt><dd>${escapeHtml(value)}</dd></div>`).join("")}</dl>
<div class="v2-device-controls" data-device-controls="${device.id}"${controlOpen ? "" : " hidden"}>
<div class="v2-device-controls-intro"><span>手动调节</span><small>参数确认后通过编排链路发送至车机</small></div>
${device.controls().map((control) => renderDeviceControl(device, control)).join("")}
<button class="v2-device-control-apply" type="button" data-device-control-apply="${device.id}"${pending || !hasDraft ? " disabled" : ""}><i data-lucide="send"></i><span>${pending ? "等待车机同步" : hasDraft ? "发送 CAN 控制" : "请先修改参数"}</span></button>
</div>
</article>`;
}).join("");
if (displaySync.activeDevice) {
$(`.v2-synced-device[data-device="${displaySync.activeDevice}"]`)?.classList.add("active");
}
displaySync.deviceFingerprint = JSON.stringify(syncedDevices.map((device) => [device.id, deviceReadoutRows(device)]));
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
}
function storeDeviceControlDraft(input) {
const deviceId = input.dataset.deviceControl;
const key = input.dataset.controlKey;
if (!deviceId || !key) return;
displaySync.deviceDrafts[deviceId] ||= {};
displaySync.deviceDrafts[deviceId][key] = input.value;
const controls = input.closest("[data-device-controls]");
const output = controls?.querySelector(`[data-control-output="${key}"]`);
if (output) output.textContent = `${input.value}${input.dataset.controlUnit || ""}`;
const card = input.closest("[data-device]");
if (card) {
card.dataset.hasDraft = "true";
const state = $("[data-device-card-state]", card);
if (state) state.textContent = "参数待发送";
}
window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-control-draft", {
detail: { device_id: deviceId, control: key, value: input.value, source: "web_console" },
}));
}
async function sendDeviceControl(deviceId) {
const device = syncedDevices.find((item) => item.id === deviceId);
if (!device) return;
if (device.controllable === false || typeof device.commands !== "function") {
window.CognitiveAgentConsole?.notify?.(`${device.name}当前只有状态反馈,尚无已验证的 CAN 控制白名单`);
return;
}
const draft = displaySync.deviceDrafts[device.id];
if (!draft || !Object.keys(draft).length) {
window.CognitiveAgentConsole?.notify?.("请先修改需要发送的参数");
return;
}
if (displaySync.pendingControls[device.id]) {
window.CognitiveAgentConsole?.notify?.("该设备的上一组 CAN 指令仍在执行");
return;
}
const values = Object.fromEntries(Object.entries(draft).map(([key, value]) => [key, String(value)]));
const commands = device.commands(values).filter(Boolean);
if (!commands.length) {
window.CognitiveAgentConsole?.notify?.("修改项没有对应的 CAN 白名单控制指令");
return;
}
const commandSummary = commands.join(";");
const detail = {
device_id: device.id,
device_name: device.name,
controls: values,
can_commands: commands,
source: "web_console",
requested_at: new Date().toISOString(),
};
const execute = window.CognitiveAgentConsole?.executeCanCommands;
if (typeof execute !== "function") {
window.CognitiveAgentConsole?.notify?.("当前页面尚未加载 CAN 直控接口");
return;
}
window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-can-control-request", { detail }));
displaySync.pendingControls[device.id] = commandSummary;
appendSyncLog({ category: "device", title: `网页端直控${device.name}`, detail: commandSummary, status: "info" });
renderDirectCanCommands(commands, device.name);
renderSyncedDevices();
try {
await execute(commands, { deviceName: device.name });
delete displaySync.deviceDrafts[device.id];
appendSyncLog({
category: "device",
title: `${device.name} CAN 控制已执行`,
detail: commandSummary,
status: "success",
});
} catch (error) {
appendSyncLog({
category: "device",
title: `${device.name} CAN 控制发送失败`,
detail: error?.message || "CAN 控制请求失败",
status: "error",
});
window.CognitiveAgentConsole?.notify?.(error?.message || "CAN 控制请求失败");
} finally {
delete displaySync.pendingControls[device.id];
renderSyncedDevices();
}
}
function scheduleSyncedDeviceRender() {
if (displaySync.deviceFrame) return;
displaySync.deviceFrame = requestAnimationFrame(() => {
displaySync.deviceFrame = null;
const fingerprint = JSON.stringify(syncedDevices.map((device) => [device.id, deviceReadoutRows(device)]));
if (fingerprint === displaySync.deviceFingerprint && $("#v2DeviceSyncGrid")?.childElementCount) return;
renderSyncedDevices();
if (displaySync.activeDevice) presentTwinDevice(displaySync.activeDevice, {
pinned: Boolean(displaySync.pinnedDevice),
selectSeat: false,
});
});
}
function appendSyncLog({ key = "", category = "sync", title, detail = "", status = "info" }) {
if (!title || (key && displaySync.logKeys.has(key))) return;
const item = {
key,
category,
title,
detail,
status,
time: new Intl.DateTimeFormat("zh-CN", {
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
}).format(new Date()),
};
if (key) displaySync.logKeys.add(key);
displaySync.logs.unshift(item);
if (displaySync.logs.length > 40) {
const removed = displaySync.logs.pop();
if (removed?.key) displaySync.logKeys.delete(removed.key);
}
renderSyncLogs();
}
function renderSyncLogs() {
const list = $("#v2SyncLogList");
if (!list) return;
const filter = $("#v2LogFilter")?.value || "all";
const items = displaySync.logs.filter((item) => filter === "all" || item.category === filter);
const icons = { task: "route", sync: "refresh-cw", extension: "usb", device: "activity" };
list.innerHTML = items.length ? items.map((item) => `
<li data-log-status="${escapeHtml(item.status)}">
<time>${escapeHtml(item.time)}</time>
<span><i data-lucide="${icons[item.category] || "circle-dot"}"></i></span>
<div><strong>${escapeHtml(item.title)}</strong><small>${escapeHtml(item.detail)}</small></div>
</li>`).join("") : '<li class="v2-sync-log-empty"><div><strong>暂无记录</strong><small>车机任务、设备反馈与外设事件会显示在这里</small></div></li>';
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
}
function panelVisible(key) {
const panel = key === "devices" ? $("#v2DevicePanel") : $("#v2ExtensionPanel");
if (panel) return !panel.classList.contains("is-collapsed");
return sessionStorage.getItem(`cognitive-agent.sync-panel.${key}`) !== "hidden";
}
function setSidePanelVisibility(key, visible) {
const panel = key === "devices" ? $("#v2DevicePanel") : $("#v2ExtensionPanel");
if (!panel) return;
const panelName = key === "devices" ? "车端设备状态" : "外设与扩展能力";
panel.hidden = !visible;
panel.classList.toggle("is-collapsed", !visible);
panel.dataset.collapsed = String(!visible);
sessionStorage.setItem(`cognitive-agent.sync-panel.${key}`, visible ? "visible" : "hidden");
$$(`[data-panel-toggle="${key}"], [data-panel-close="${key}"]`).forEach((button) => {
button.classList.toggle("active", visible);
button.setAttribute("aria-pressed", String(visible));
button.setAttribute("aria-expanded", String(visible));
button.setAttribute("aria-label", `${visible ? "收起" : "展开"}${panelName}`);
button.title = `${visible ? "收起" : "展开"}${panelName}`;
const actionLabel = $("[data-panel-action-label]", button);
if (actionLabel) actionLabel.textContent = visible ? "收起" : "展开";
const stateLabel = $("[data-panel-state-label]", button);
if (stateLabel) stateLabel.textContent = visible ? "已展开" : "已收起";
});
const openPanels = ["devices", "extensions"].filter((panelKey) => {
const candidate = panelKey === "devices" ? $("#v2DevicePanel") : $("#v2ExtensionPanel");
return candidate && !candidate.classList.contains("is-collapsed");
});
$(".joint-twin-shell")?.setAttribute("data-open-panels", openPanels.join(" ") || "none");
const rightRail = $(".v2-unified-right-rail");
if (rightRail) {
const collapsed = openPanels.length === 0;
rightRail.hidden = collapsed;
rightRail.classList.toggle("is-collapsed", collapsed);
rightRail.toggleAttribute("inert", collapsed);
rightRail.dataset.openPanels = openPanels.join(" ") || "none";
}
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
window.dispatchEvent(new Event("resize"));
}
const twinPopoverClamp = (value, minimum, maximum) => (
maximum < minimum ? minimum : Math.max(minimum, Math.min(maximum, value))
);
function twinPopoverIntersectionArea(first, second) {
const width = Math.max(0, Math.min(first.right, second.right) - Math.max(first.left, second.left));
const height = Math.max(0, Math.min(first.bottom, second.bottom) - Math.max(first.top, second.top));
return width * height;
}
function positionTwinDevicePopover(deviceId = displaySync.activeDevice) {
const stage = $("#seatTwin");
const popover = $("#v2TwinDevicePopover");
const hotspot = deviceId
? $(`[data-twin-device="${deviceId}"]`, stage)
: null;
const hotspotLabel = hotspot ? $(".v2-twin-hotspot-label", hotspot) : null;
if (!stage || !popover || popover.hidden || !hotspot || !hotspotLabel) return null;
if (hotspot.hidden) {
popover.dataset.anchorVisible = "false";
if (popover.dataset.pinned === "false") popover.hidden = true;
return null;
}
popover.dataset.anchorVisible = "true";
const stageRect = stage.getBoundingClientRect();
const labelClientRect = hotspotLabel.getBoundingClientRect();
const popoverWidth = popover.offsetWidth;
const popoverHeight = popover.offsetHeight;
if (!stageRect.width || !stageRect.height || !popoverWidth || !popoverHeight) return null;
const margin = stageRect.width < 520 ? 8 : 12;
const gap = stageRect.width < 520 ? 8 : 12;
const bounds = {
left: margin,
top: margin,
right: Math.max(margin, stageRect.width - margin),
bottom: Math.max(margin, stageRect.height - margin),
};
const labelRect = {
left: labelClientRect.left - stageRect.left,
top: labelClientRect.top - stageRect.top,
right: labelClientRect.right - stageRect.left,
bottom: labelClientRect.bottom - stageRect.top,
};
labelRect.width = labelRect.right - labelRect.left;
labelRect.height = labelRect.bottom - labelRect.top;
labelRect.centerX = (labelRect.left + labelRect.right) / 2;
labelRect.centerY = (labelRect.top + labelRect.bottom) / 2;
const toStageRect = (element, padding = 0) => {
const rect = element.getBoundingClientRect();
return {
left: rect.left - stageRect.left - padding,
top: rect.top - stageRect.top - padding,
right: rect.right - stageRect.left + padding,
bottom: rect.bottom - stageRect.top + padding,
};
};
const visibleElement = (element) => {
const rect = element.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
const labelObstacles = $$(".v2-twin-hotspot-label", stage)
.filter((element) => element !== hotspotLabel && visibleElement(element))
.map((element) => ({
deviceId: element.closest("[data-twin-device]")?.dataset.twinDevice || "",
rect: toStageRect(element, 3),
}));
const chromeObstacles = $$([".twin-seat-label", ".twin-orientation", ".twin-readout:not([hidden])"].join(","), stage)
.filter(visibleElement)
.map((element) => toStageRect(element, 5));
const activeLabelObstacle = {
left: labelRect.left - 10,
top: labelRect.top - 10,
right: labelRect.right + 10,
bottom: labelRect.bottom + 10,
};
const geometrySignature = [
deviceId,
popover.dataset.pinned,
Math.round(stageRect.width),
Math.round(stageRect.height),
Math.round(popoverWidth),
Math.round(popoverHeight),
Math.round(labelRect.left),
Math.round(labelRect.top),
...labelObstacles.flatMap(({ deviceId: obstacleDeviceId, rect }) => [
obstacleDeviceId,
Math.round(rect.left),
Math.round(rect.top),
]),
...chromeObstacles.flatMap((rect) => [Math.round(rect.left), Math.round(rect.top)]),
].join(":");
if (popover.dataset.layoutSignature === geometrySignature) return null;
popover.dataset.layoutSignature = geometrySignature;
const availableSpace = {
right: bounds.right - labelRect.right,
left: labelRect.left - bounds.left,
bottom: bounds.bottom - labelRect.bottom,
top: labelRect.top - bounds.top,
};
const sideOrder = Object.keys(availableSpace).sort((first, second) => (
availableSpace[second] - availableSpace[first]
));
const rawCandidates = [];
const pushSideCandidates = (side) => {
const horizontal = side === "left" || side === "right";
const alignments = horizontal
? [
[labelRect.centerY - popoverHeight / 2, "center"],
[labelRect.top, "start"],
[labelRect.bottom - popoverHeight, "end"],
]
: [
[labelRect.centerX - popoverWidth / 2, "center"],
[labelRect.left, "start"],
[labelRect.right - popoverWidth, "end"],
];
alignments.forEach(([crossAxis, alignment], alignmentIndex) => {
let left = crossAxis;
let top = crossAxis;
if (side === "right") {
left = labelRect.right + gap;
top = crossAxis;
} else if (side === "left") {
left = labelRect.left - gap - popoverWidth;
top = crossAxis;
} else if (side === "bottom") {
left = crossAxis;
top = labelRect.bottom + gap;
} else {
left = crossAxis;
top = labelRect.top - gap - popoverHeight;
}
rawCandidates.push({ left, top, side, alignment, alignmentIndex, anchored: true });
});
};
sideOrder.forEach(pushSideCandidates);
const maximumLeft = bounds.right - popoverWidth;
const maximumTop = bounds.bottom - popoverHeight;
[bounds.left, labelRect.left - gap - popoverWidth, labelRect.centerX - popoverWidth / 2,
labelRect.right + gap, maximumLeft].forEach((left) => {
[bounds.top, labelRect.top - gap - popoverHeight, labelRect.centerY - popoverHeight / 2,
labelRect.bottom + gap, maximumTop].forEach((top) => {
rawCandidates.push({ left, top, side: "adaptive", alignment: "free", alignmentIndex: 0, anchored: false });
});
});
const seen = new Set();
const previousPlacement = popover.dataset.anchorDevice === deviceId
? `${popover.dataset.placement || ""}:${popover.dataset.alignment || ""}`
: "";
const candidates = rawCandidates.map((candidate) => {
const left = twinPopoverClamp(candidate.left, bounds.left, maximumLeft);
const top = twinPopoverClamp(candidate.top, bounds.top, maximumTop);
const key = `${Math.round(left)}:${Math.round(top)}`;
if (seen.has(key)) return null;
seen.add(key);
const rect = {
left,
top,
right: left + popoverWidth,
bottom: top + popoverHeight,
};
const clampDistance = Math.abs(left - candidate.left) + Math.abs(top - candidate.top);
const activeOverlap = twinPopoverIntersectionArea(rect, activeLabelObstacle);
const shadeOverlap = labelObstacles.reduce((total, obstacle) => (
total + (obstacle.deviceId === "shade" ? twinPopoverIntersectionArea(rect, obstacle.rect) : 0)
), 0);
const labelOverlap = labelObstacles.reduce((total, obstacle) => (
total + (obstacle.deviceId === "shade" ? 0 : twinPopoverIntersectionArea(rect, obstacle.rect))
), 0);
const chromeOverlap = chromeObstacles.reduce((total, obstacle) => (
total + twinPopoverIntersectionArea(rect, obstacle)
), 0);
const distance = Math.hypot(
left + popoverWidth / 2 - labelRect.centerX,
top + popoverHeight / 2 - labelRect.centerY,
);
const placement = candidate.side === "adaptive"
? (Math.abs(left + popoverWidth / 2 - labelRect.centerX) > Math.abs(top + popoverHeight / 2 - labelRect.centerY)
? (left + popoverWidth / 2 >= labelRect.centerX ? "right" : "left")
: (top + popoverHeight / 2 >= labelRect.centerY ? "bottom" : "top"))
: candidate.side;
const placementKey = `${placement}:${candidate.alignment}`;
const sideRank = sideOrder.indexOf(placement);
const score = activeOverlap * 1000
+ shadeOverlap * 640
+ labelOverlap * 36
+ chromeOverlap * 12
+ clampDistance * 90
+ distance * 0.35
+ Math.max(0, sideRank) * 70
+ candidate.alignmentIndex * 8
+ (candidate.anchored ? 0 : 90)
- (placementKey === previousPlacement ? 28 : 0);
return { ...candidate, left, top, placement, score, activeOverlap, shadeOverlap, labelOverlap };
}).filter(Boolean);
const best = candidates.sort((first, second) => first.score - second.score)[0];
if (!best) return null;
popover.style.left = `${Math.round(best.left)}px`;
popover.style.top = `${Math.round(best.top)}px`;
popover.style.removeProperty("right");
popover.style.removeProperty("bottom");
popover.dataset.anchorDevice = deviceId;
popover.dataset.placement = best.placement;
popover.dataset.alignment = best.alignment;
popover.dataset.shadeOverlap = String(Math.round(best.shadeOverlap));
popover.dataset.labelOverlap = String(Math.round(best.labelOverlap));
return best;
}
function presentTwinDevice(deviceId, { pinned = false, selectSeat = pinned } = {}) {
const device = syncedDevices.find((item) => item.id === deviceId);
const popover = $("#v2TwinDevicePopover");
if (!device || !popover) return;
if (displaySync.previewClearTimer) {
window.clearTimeout(displaySync.previewClearTimer);
displaySync.previewClearTimer = null;
}
displaySync.activeDevice = deviceId;
if (pinned) displaySync.pinnedDevice = deviceId;
const rows = deviceReadoutRows(device);
popover.hidden = false;
popover.dataset.device = deviceId;
popover.dataset.pinned = String(Boolean(displaySync.pinnedDevice));
popover.innerHTML = `
<header><span><i data-lucide="${device.icon}"></i></span><div><strong>${escapeHtml(device.name)}</strong></div><b>${pinned ? "已固定" : "靠近查看"}</b><button class="v2-twin-popover-close" type="button" data-twin-popover-close aria-label="关闭${escapeHtml(device.name)}状态详情" title="关闭"><i data-lucide="x"></i></button></header>
<dl>${rows.map(([label, value]) => `<div><dt>${escapeHtml(label)}</dt><dd>${escapeHtml(value)}</dd></div>`).join("")}</dl>`;
$$("[data-twin-device]").forEach((button) => button.classList.toggle("active", button.dataset.twinDevice === deviceId));
$$(".v2-synced-device").forEach((button) => button.classList.toggle("active", button.dataset.device === deviceId));
if (selectSeat && ["driver", "passenger"].includes(deviceId)) {
$(`[data-twin-seat="${deviceId}"]`)?.click();
}
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
positionTwinDevicePopover(deviceId);
}
function clearTwinDevicePreview({ force = false } = {}) {
if (displaySync.previewClearTimer) {
window.clearTimeout(displaySync.previewClearTimer);
displaySync.previewClearTimer = null;
}
if (displaySync.pinnedDevice && !force) return;
displaySync.activeDevice = null;
if (force) displaySync.pinnedDevice = null;
const popover = $("#v2TwinDevicePopover");
if (popover) popover.hidden = true;
$$("[data-twin-device], .v2-synced-device").forEach((button) => button.classList.remove("active"));
}
function scheduleTwinDevicePreviewClear() {
if (displaySync.pinnedDevice) return;
if (displaySync.previewClearTimer) window.clearTimeout(displaySync.previewClearTimer);
displaySync.previewClearTimer = window.setTimeout(() => {
displaySync.previewClearTimer = null;
clearTwinDevicePreview();
}, 45);
}
function buildTwinDeviceInteractions() {
const stage = $("#seatTwin");
if (!stage || $("#v2TwinHotspots")) return;
const hotspots = make("div", "v2-twin-hotspots", syncedDevices.map((device) => `
<button type="button" data-twin-device="${device.id}" aria-label="查看${device.name}状态"><span class="v2-twin-hotspot-label"><i data-lucide="${device.icon}"></i><b>${device.name}</b><small>实时状态</small></span></button>`).join(""));
hotspots.id = "v2TwinHotspots";
const popover = make("aside", "v2-twin-device-popover", "");
popover.id = "v2TwinDevicePopover";
popover.hidden = true;
popover.setAttribute("role", "dialog");
popover.setAttribute("aria-label", "车端设备状态详情");
popover.setAttribute("aria-live", "polite");
stage.append(hotspots, popover);
hotspots.addEventListener("pointerdown", (event) => event.stopPropagation());
$$("[data-twin-device]", hotspots).forEach((button) => {
button.addEventListener("pointerenter", () => {
if (!displaySync.pinnedDevice && performance.now() >= displaySync.hoverSuppressedUntil) {
presentTwinDevice(button.dataset.twinDevice);
}
});
button.addEventListener("pointerleave", (event) => {
if (event.relatedTarget?.closest?.("#v2TwinDevicePopover")) return;
scheduleTwinDevicePreviewClear();
});
button.addEventListener("focus", () => presentTwinDevice(button.dataset.twinDevice));
button.addEventListener("blur", scheduleTwinDevicePreviewClear);
});
hotspots.addEventListener("click", (event) => {
const button = event.target.closest("[data-twin-device]");
if (!button) return;
event.stopPropagation();
const samePinned = displaySync.pinnedDevice === button.dataset.twinDevice;
if (samePinned) clearTwinDevicePreview({ force: true });
else presentTwinDevice(button.dataset.twinDevice, { pinned: true });
});
popover.addEventListener("pointerdown", (event) => event.stopPropagation());
popover.addEventListener("pointerenter", () => {
if (displaySync.previewClearTimer) {
window.clearTimeout(displaySync.previewClearTimer);
displaySync.previewClearTimer = null;
}
});
popover.addEventListener("pointerleave", scheduleTwinDevicePreviewClear);
popover.addEventListener("click", (event) => {
if (!event.target.closest("[data-twin-popover-close]")) return;
event.stopPropagation();
displaySync.hoverSuppressedUntil = performance.now() + 160;
clearTwinDevicePreview({ force: true });
});
stage.addEventListener("click", (event) => {
if (event.target.closest("[data-twin-device], #v2TwinDevicePopover")) return;
clearTwinDevicePreview({ force: true });
});
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
}
function taskKind(utterance, result, source = "") {
if (source === "web_console") return "网页远程编排";
if (result?.mode_profile) return "自定义功能模式";
const preset = $$(".mode-chips [data-mode-command]").find((button) => (
String(utterance).includes(button.textContent.trim())
|| button.dataset.modeCommand === utterance
));
return preset ? "预设功能模式" : utterance ? "一句话自定义编排" : "等待编排任务";
}
function renderTaskTimeline(task) {
const timeline = $("#v2TaskTimeline");
if (!timeline) return;
const result = task?.result;
const trace = Array.isArray(result?.trace_events) ? result.trace_events : [];
const domains = Object.keys(result?.function_configs || {});
const stages = task?.status === "running"
? ["接收编排指令", "解析意图", "构建功能模式", "等待设备反馈"]
: trace.length
? trace.slice(-4).map((item) => item.label || item.stage || item.event || "执行步骤")
: domains.length
? ["接收编排指令", `编排 ${domains.length} 个设备域`, "生成执行计划", "同步执行结果"]
: ["等待车机或网页端下发编排任务"];
timeline.innerHTML = stages.map((stage, index) => `
<span class="${task?.status === "running" && index === stages.length - 1 ? "active" : "done"}">
<i>${String(index + 1).padStart(2, "0")}</i><b>${escapeHtml(stage)}</b>
</span>`).join("");
}
function setTaskPresentation(task = {}) {
const utterance = String(task.utterance || "");
const result = task.result && typeof task.result === "object" ? task.result : null;
const input = $("#utterance");
if (input && utterance && document.activeElement !== input
&& !document.body.classList.contains("assistant-chat-enabled")) input.value = utterance;
const kind = taskKind(utterance, result, String(task.source || ""));
if ($("#v2TaskKind")) $("#v2TaskKind").textContent = kind;
if ($("#v2TaskCommand")) $("#v2TaskCommand").textContent = utterance || "等待车机端或网页端选择预设模式、输入一句话指令";
const normalizedStatus = String(task.status || "idle").toLowerCase();
const verificationStatus = String(
result?.execution_outcome
|| result?.execution_report?.spec?.verification_status
|| "",
);
const submittedUnverified = verificationStatus === "submitted_unverified";
const statusMap = {
idle: ["等待任务", "idle"],
running: ["构建 / 执行中", "running"],
succeeded: ["执行完成", "succeeded"],
partially_succeeded: ["部分完成", "partial"],
needs_confirmation: ["等待确认", "partial"],
failed: ["执行失败", "failed"],
};
const [label, state] = submittedUnverified && normalizedStatus === "succeeded"
? ["指令已提交 · 等待状态确认", "partial"]
: statusMap[normalizedStatus] || statusMap.idle;
const badge = $("#v2TaskState");
if (badge) {
badge.textContent = label;
badge.dataset.state = state;
}
$$(".mode-chips [data-mode-command]").forEach((button) => {
const active = Boolean(utterance) && (
utterance === button.dataset.modeCommand
|| utterance.includes(button.textContent.trim())
);
button.classList.toggle("v2-sync-active", active);
});
renderTaskTimeline(task);
if (normalizedStatus === "running") {
// The streaming endpoint can publish fully validated commands before the
// slower display-sync poll observes the terminal execution state. A stale
// `running` snapshot for the same task must not erase those commands.
if (!validatedCommandsBelongToTask(task)) {
resetValidatedCommandContext();
renderParsedCanCommands(null, { message: "正在解析自然语言并生成控制指令…" });
}
} else if (result) {
const commands = parsedCanCommands(result);
if (commands.length) {
validatedCommandsVisible = true;
rememberValidatedCommandContext(result);
renderParsedCanCommands(result);
} else if (!validatedCommandsBelongToTask(task)) {
resetValidatedCommandContext();
renderParsedCanCommands(result);
}
}
}
function applyDisplaySnapshot(snapshot) {
const synchronizedCatalog = Array.isArray(snapshot?.extension?.catalog)
? snapshot.extension.catalog
: [];
const revision = Number(snapshot?.revision ?? -1);
if (revision !== displaySync.revision) {
displaySync.revision = revision;
const task = snapshot?.task || {};
setTaskPresentation(task);
const sourceLabel = task.source === "web_console" ? "网页端" : "车机端";
if (task.utterance) {
const submittedUnverified = task.result?.execution_outcome === "submitted_unverified"
|| task.result?.execution_report?.spec?.verification_status === "submitted_unverified";
appendSyncLog({
key: `task:${revision}:${task.status}`,
category: "task",
title: `${sourceLabel}${task.status === "running" ? "下发编排任务" : "任务状态已同步"}`,
detail: `${task.utterance} · ${submittedUnverified ? "指令已提交,等待车辆状态确认" : task.status || "idle"}`,
status: submittedUnverified ? "info" : ["succeeded", "partially_succeeded"].includes(task.status) ? "success" : task.status === "failed" ? "error" : "info",
});
}
const result = task.result;
const requestId = result?.request_id || null;
if (result && requestId !== displaySync.requestId) {
displaySync.requestId = requestId;
window.CognitiveAgentConsole?.applySyncedResult?.(result, {
utterance: task.utterance || "",
});
}
}
const extensionFingerprint = synchronizedCatalog
.map((item) => `${item.device_id || "external"}:${item.action || item.name || "capability"}`)
.sort()
.join("|");
if (extensionFingerprint && extensionFingerprint !== displaySync.extensionFingerprint) {
displaySync.extensionFingerprint = extensionFingerprint;
appendSyncLog({
key: `extension:${extensionFingerprint}`,
category: "extension",
title: "外设能力目录已同步",
detail: `${new Set(synchronizedCatalog.map((item) => item.device_id || "external")).size} 个外设 · ${synchronizedCatalog.length} 项能力`,
status: "success",
});
}
window.dispatchEvent(new CustomEvent("cognitive-agent:display-sync", { detail: snapshot }));
}
async function pollVehicleDisplaySync() {
try {
const response = await fetch("/api/vehicle-display-sync", {
credentials: "same-origin",
cache: "no-store",
headers: { Accept: "application/json" },
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
applyDisplaySnapshot(await response.json());
appendSyncLog({ key: "sync:online", category: "sync", title: "网页同步链路已建立", detail: "车机任务与设备状态每 2 秒同步", status: "success" });
} catch (_error) {
console.warn("[vehicle-display-sync]", _error);
}
}
function enableRemoteOrchestration() {
const form = $("#requestForm");
const input = $("#utterance");
if (input) {
input.readOnly = false;
input.removeAttribute("aria-readonly");
input.setAttribute("aria-label", "输入问题或座舱控制需求");
input.placeholder = "输入问题或座舱控制需求";
}
form?.addEventListener("submit", () => {
const utterance = input?.value.trim();
if (utterance) appendSyncLog({
category: "task",
title: "网页端发送助手请求",
detail: utterance,
status: "info",
});
});
$$("[data-mode-command]").forEach((button) => {
button.removeAttribute("aria-disabled");
button.title = "点击后由网页端发送至车机执行";
});
}
function syncSidebarLogBaseline() {
const sidebar = $(".v2-sidebar");
const lowerPanel = $("#v2DevicePanel") || $(".joint-twin-shell > .twin-band");
if (!sidebar || !lowerPanel || ui.sidebarCollapsed) return;
const sidebarRect = sidebar.getBoundingClientRect();
const overview = $(".v2-sync-sidebar-overview", sidebar);
const overviewRect = overview?.getBoundingClientRect();
const overviewBottom = overview && overviewRect
? Array.from(overview.children).reduce(
(bottom, child) => Math.max(bottom, child.getBoundingClientRect().bottom),
overviewRect.top,
)
: 0;
const overviewPaddingBottom = overview
? Number.parseFloat(getComputedStyle(overview).paddingBottom) || 0
: 0;
const requiredOverviewHeight = overviewRect
? Math.ceil(overviewBottom - overviewRect.top + overviewPaddingBottom + 10)
: 0;
const offset = Math.max(
0,
lowerPanel.getBoundingClientRect().top - sidebarRect.top,
requiredOverviewHeight,
);
sidebar.style.setProperty("--v2-sync-log-top", `${offset}px`);
}
function buildSyncControlLayer() {
document.body.classList.add("v2-passive-display", "v2-interactive-sync");
document.body.dataset.module = "self-orchestration";
enableRemoteOrchestration();
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>
<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>
</section>`);
if (sidebar && sidebarFooter) {
sidebar.insertBefore(syncSidebar, sidebarFooter);
const syncOverview = $(".v2-sync-sidebar-overview", syncSidebar);
const transportField = $("#vehicleTransportField");
const executionField = $(".field-group:has(.execution-mode-options)");
const simulationFaultField = $("#simulationFaultField");
if (syncOverview) {
const insertVisibleControl = (field, className) => {
if (!field) return;
field.classList.add(className);
syncOverview.append(field);
};
insertVisibleControl(transportField, "v2-sync-transport-field");
if (executionField) executionField.hidden = false;
insertVisibleControl(executionField, "v2-sync-execution-field");
insertVisibleControl(simulationFaultField, "v2-sync-simulation-fault-field");
const canCommandPanel = make("details", "v2-can-command-panel", `
<summary>
<span><i data-lucide="binary"></i><b>已解析控制指令</b></span>
<strong id="v2CanCommandCount">0</strong>
<i data-lucide="chevron-down"></i>
</summary>
<div class="v2-can-command-list" id="v2CanCommandList">
<p>等待自然语言解析结果</p>
</div>`);
canCommandPanel.id = "v2CanCommandPanel";
syncOverview.append(canCommandPanel);
canCommandPanel.addEventListener("toggle", () => {
requestAnimationFrame(syncSidebarLogBaseline);
});
}
syncOverview?.append(sidebarFooter);
}
const naturalPanel = $("#v2NaturalPanel");
const taskOverview = make("section", "v2-task-overview", `
<div class="v2-task-copy"><span id="v2TaskKind">当前车机任务</span><strong id="v2TaskCommand">等待车机端或网页端选择预设模式、输入一句话指令</strong></div>
<span class="v2-task-state" id="v2TaskState" data-state="idle">等待任务</span>
<div class="v2-task-timeline" id="v2TaskTimeline"><span><i>01</i><b>等待车机或网页端下发编排任务</b></span></div>`);
naturalPanel?.append(taskOverview);
const jointShell = $(".joint-twin-shell");
const cabinBand = $(".cabin-band", jointShell);
if (jointShell && cabinBand) {
const rightRail = make("aside", "v2-unified-right-rail", `
<div class="v2-side-status-stack">
<section class="v2-device-sync-board v2-side-status-panel" id="v2DevicePanel">
<header><div><h3>车端设备状态</h3></div><span><i></i>实时反馈</span></header>
<div class="v2-device-sync-grid" id="v2DeviceSyncGrid"></div>
</section>
<section class="v2-extension-panel v2-side-status-panel" id="v2ExtensionPanel"><div class="v2-extension-sync-mount" id="vehicleExtensionSync" aria-live="polite"></div></section>
</div>`);
jointShell.insertBefore(rightRail, cabinBand);
const panelDock = $("#v2PanelControlDock");
if (panelDock) {
const deviceToggle = make("button", "v2-panel-control-button", '<i data-lucide="car-front"></i><span class="visually-hidden">车端设备状态</span>');
deviceToggle.type = "button";
deviceToggle.dataset.panelToggle = "devices";
deviceToggle.setAttribute("aria-controls", "v2DevicePanel");
deviceToggle.setAttribute("aria-pressed", "false");
deviceToggle.setAttribute("aria-expanded", "false");
deviceToggle.setAttribute("aria-label", "展开车端设备状态");
deviceToggle.title = "展开车端设备状态";
const extensionToggle = make("button", "v2-panel-control-button", '<i data-lucide="usb"></i><span class="visually-hidden">外设与扩展能力</span>');
extensionToggle.type = "button";
extensionToggle.dataset.panelToggle = "extensions";
extensionToggle.setAttribute("aria-controls", "v2ExtensionPanel");
extensionToggle.setAttribute("aria-pressed", "false");
extensionToggle.setAttribute("aria-expanded", "false");
extensionToggle.setAttribute("aria-label", "展开外设与扩展能力");
extensionToggle.title = "展开外设与扩展能力";
panelDock.append(deviceToggle, extensionToggle);
}
buildTwinDeviceInteractions();
cabinBand.classList.add("v2-source-state-only");
new MutationObserver(scheduleSyncedDeviceRender).observe(cabinBand, {
attributes: true,
childList: true,
characterData: true,
subtree: true,
});
const stateBand = $(".state-band");
if (stateBand) new MutationObserver(scheduleSyncedDeviceRender).observe(stateBand, {
attributes: true,
childList: true,
characterData: true,
subtree: true,
});
}
setSidePanelVisibility("devices", false);
setSidePanelVisibility("extensions", false);
window.addEventListener("resize", syncSidebarLogBaseline);
requestAnimationFrame(syncSidebarLogBaseline);
document.addEventListener("click", (event) => {
const toggle = event.target.closest("[data-panel-toggle], [data-panel-close]");
if (toggle) {
const key = toggle.dataset.panelToggle || toggle.dataset.panelClose;
setSidePanelVisibility(key, !panelVisible(key));
}
});
const deviceGrid = $("#v2DeviceSyncGrid");
deviceGrid?.addEventListener("click", (event) => {
const controlToggle = event.target.closest("[data-device-control-toggle]");
if (controlToggle) {
const deviceId = controlToggle.dataset.deviceControlToggle;
displaySync.controlDevice = displaySync.controlDevice === deviceId ? null : deviceId;
if (displaySync.controlDevice) presentTwinDevice(deviceId, { pinned: true });
else if (displaySync.pinnedDevice === deviceId) clearTwinDevicePreview({ force: true });
renderSyncedDevices();
return;
}
const apply = event.target.closest("[data-device-control-apply]");
if (apply) {
sendDeviceControl(apply.dataset.deviceControlApply);
return;
}
const focus = event.target.closest("[data-device-focus]");
if (focus) {
const samePinned = displaySync.pinnedDevice === focus.dataset.deviceFocus;
if (samePinned) clearTwinDevicePreview({ force: true });
else presentTwinDevice(focus.dataset.deviceFocus, { pinned: true });
}
});
deviceGrid?.addEventListener("input", (event) => {
if (event.target.matches("[data-device-control]")) storeDeviceControlDraft(event.target);
});
deviceGrid?.addEventListener("change", (event) => {
if (event.target.matches("select[data-device-control]")) storeDeviceControlDraft(event.target);
});
$("#v2LogFilter")?.addEventListener("change", renderSyncLogs);
$("#requestForm")?.addEventListener("submit", () => {
resetValidatedCommandContext();
renderParsedCanCommands(null, { message: "正在解析自然语言并生成控制指令…" });
});
window.addEventListener("cognitive-agent:validated-can-commands", (event) => {
const result = event.detail || {};
validatedCommandsVisible = parsedCanCommands(result).length > 0;
if (validatedCommandsVisible) rememberValidatedCommandContext(result);
else resetValidatedCommandContext();
renderParsedCanCommands(result, {
message: "控制指令已解析,正在等待传输结果…",
});
});
window.addEventListener("cognitive-agent:result", (event) => {
const result = event.detail || {};
const submittedUnverified = result.execution_outcome === "submitted_unverified"
|| result.execution_report?.spec?.verification_status === "submitted_unverified";
const commands = parsedCanCommands(result);
if (commands.length) {
validatedCommandsVisible = true;
rememberValidatedCommandContext(result);
renderParsedCanCommands(result);
} else if (!validatedCommandsBelongToTask({
utterance: result.utterance,
result,
})) {
resetValidatedCommandContext();
renderParsedCanCommands(result);
}
appendSyncLog({
key: result.request_id ? `result:${result.request_id}` : "",
category: "task",
title: submittedUnverified
? "CAN 指令已提交,等待车辆状态确认"
: ["succeeded", "partially_succeeded"].includes(result.status)
? "编排任务执行完成"
: "编排任务返回结果",
detail: `${result.summary || result.message || "执行反馈已同步"} · ${submittedUnverified ? "submitted_unverified" : result.status || "unknown"}`,
status: submittedUnverified ? "info" : ["succeeded", "partially_succeeded"].includes(result.status) ? "success" : result.status === "failed" ? "error" : "info",
});
});
window.addEventListener("cognitive-agent:request-complete", (event) => {
if (event.detail?.error && !validatedCommandsVisible) {
renderParsedCanCommands(null, { message: "本次请求未生成控制指令" });
}
if (event.detail?.error) {
const error = event.detail.error;
const outcomeLabels = {
failed_or_blocked: "失败或已阻止",
submitted_unverified: "已提交但未验证",
verified_success: "反馈验证成功",
};
const outcome = outcomeLabels[error.outcome];
appendSyncLog({
category: "task",
title: "编排任务执行失败",
detail: `${error.detail || error.message || "请求失败"}${outcome ? ` · ${outcome}` : ""}`,
status: "error",
});
}
});
window.addEventListener("cognitive-agent:connection", (event) => {
const online = event.detail?.status === "online";
appendSyncLog({
key: `connection:${event.detail?.status}`,
category: "sync",
title: online ? "车机服务连接正常" : "车机服务连接异常",
detail: online ? "远程控制与状态同步通道可用" : String(event.detail?.status || "offline"),
status: online ? "success" : "error",
});
});
renderSyncedDevices();
setTaskPresentation({ status: "idle" });
appendSyncLog({ key: "client:ready", category: "sync", title: "同步控制台已就绪", detail: "正在连接车机任务与设备状态服务", status: "info" });
pollVehicleDisplaySync();
displaySync.pollTimer = window.setInterval(pollVehicleDisplaySync, 2000);
const updateClock = () => {
if ($("#v2SyncClock")) $("#v2SyncClock").textContent = new Intl.DateTimeFormat("zh-CN", {
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
}).format(new Date());
};
updateClock();
window.setInterval(updateClock, 1000);
}
function setPage(pageIndex) {
ui.activePage = 0;
const definition = pageDefinitions[ui.activePage];
$(".v2-output-shell")?.classList.toggle("v2-twin-active", definition.index === 0);
$$(".v2-output-page").forEach((page) => {
const active = Number(page.dataset.v2Page) === ui.activePage;
page.classList.toggle("v2-active-output", active);
page.setAttribute("aria-hidden", active ? "false" : "true");
});
$$(".v2-page-button").forEach((button) => {
const active = Number(button.dataset.v2PageTarget) === ui.activePage;
button.classList.toggle("active", active);
button.setAttribute("aria-current", active ? "page" : "false");
});
$("#v2PageIndex").textContent = String(definition.index).padStart(2, "0");
$("#v2PageTitle").textContent = definition.label;
$("#v2OutputState").textContent = definition.index === 0 ? "LIVE STATE" : "AUDIT VIEW";
requestAnimationFrame(updateScrollCues);
}
function updateScrollCues() {
$$(".result-workspace .tab-panel").forEach((panel) => {
const active = panel.classList.contains("v2-active-output");
panel.classList.toggle(
"v2-scrollable",
active && panel.scrollHeight > panel.clientHeight + 6,
);
});
}
function updateModeEdgeHints() {
const chips = $(".v2-mode-rail .mode-chips");
if (!chips) return;
const buttons = $$('button', chips);
buttons.forEach((button) => {
button.classList.remove("v2-edge-fade-left", "v2-edge-fade-right");
button.style.removeProperty("--v2-edge-fade-start");
button.style.removeProperty("--v2-edge-fade-end");
});
if (buttons.length < 2 || chips.clientWidth <= 0) return;
const viewport = chips.getBoundingClientRect();
const visible = buttons.map((button) => ({
button,
rect: button.getBoundingClientRect(),
})).filter(({ rect }) => {
return rect.right > viewport.left + 1 && rect.left < viewport.right - 1;
});
if (!visible.length) return;
const fadeSpan = 30;
const firstVisible = visible[0];
const lastVisible = visible[visible.length - 1];
if (firstVisible.rect.left < viewport.left - 1) {
const clippedAt = Math.min(100, Math.max(
0,
((viewport.left - firstVisible.rect.left) / firstVisible.rect.width) * 100,
));
firstVisible.button.style.setProperty("--v2-edge-fade-start", `${clippedAt}%`);
firstVisible.button.style.setProperty(
"--v2-edge-fade-end",
`${Math.min(100, clippedAt + fadeSpan)}%`,
);
firstVisible.button.classList.add("v2-edge-fade-left");
}
if (lastVisible.rect.right > viewport.right + 1) {
const clippedAt = Math.min(100, Math.max(
0,
((viewport.right - lastVisible.rect.left) / lastVisible.rect.width) * 100,
));
lastVisible.button.style.setProperty(
"--v2-edge-fade-start",
`${Math.max(0, clippedAt - fadeSpan)}%`,
);
lastVisible.button.style.setProperty("--v2-edge-fade-end", `${clippedAt}%`);
lastVisible.button.classList.add("v2-edge-fade-right");
}
}
function setControlMode(mode, { triggerGesture = true } = {}) {
ui.controlMode = mode === "gesture" ? "gesture" : "natural";
sessionStorage.setItem("cognitive-agent.control-mode", ui.controlMode);
document.body.dataset.controlMode = ui.controlMode;
const requestStatus = $(".v2-request-status");
const statusTarget = ui.controlMode === "gesture"
? $("#v2GestureExecutionSlot")
: $("#v2NaturalGrid");
if (requestStatus && statusTarget && requestStatus.parentElement !== statusTarget) {
statusTarget.append(requestStatus);
}
const drawer = $("#gestureDrawer");
if (!triggerGesture) {
const gestureVisible = ui.controlMode === "gesture";
drawer?.setAttribute("aria-hidden", String(!gestureVisible));
drawer?.classList.toggle("open", gestureVisible);
return;
}
if (ui.controlMode === "gesture") {
window.CognitiveAgentGesture?.open?.();
} else if (drawer?.classList.contains("open")) {
window.CognitiveAgentGesture?.close?.();
}
}
function setModule(module, { persist = true } = {}) {
const allowed = new Set(["self-orchestration", "self-extension"]);
ui.activeModule = allowed.has(module) ? module : "self-orchestration";
if (persist) {
sessionStorage.setItem("cognitive-agent.active-module", ui.activeModule);
}
document.body.dataset.module = ui.activeModule;
$$('.v2-module-tab').forEach((button) => {
const active = button.dataset.module === ui.activeModule;
button.classList.toggle("active", active);
button.setAttribute("aria-selected", String(active));
button.tabIndex = active ? 0 : -1;
});
if (ui.activeModule === "self-extension") {
const drawer = $("#gestureDrawer");
if (drawer?.classList.contains("open")) $("#gestureCloseButton")?.click();
} else {
setControlMode(ui.controlMode, { triggerGesture: false });
}
window.dispatchEvent(new CustomEvent("cognitive-agent:module-change", {
detail: { module: ui.activeModule },
}));
window.setTimeout(() => window.dispatchEvent(new Event("resize")), 120);
}
function setSidebarCollapsed(collapsed) {
ui.sidebarCollapsed = Boolean(collapsed);
sessionStorage.setItem(
"cognitive-agent.sidebar-collapsed",
String(ui.sidebarCollapsed),
);
document.body.classList.toggle("v2-sidebar-collapsed", ui.sidebarCollapsed);
const sidebar = $("#v2SidebarPanel");
if (sidebar) {
sidebar.setAttribute("aria-hidden", String(ui.sidebarCollapsed));
sidebar.toggleAttribute("inert", ui.sidebarCollapsed);
}
const button = $("#v2SidebarToggle");
if (button) {
const expanded = !ui.sidebarCollapsed;
button.classList.toggle("active", expanded);
button.setAttribute("aria-pressed", String(expanded));
button.setAttribute("aria-expanded", String(!ui.sidebarCollapsed));
const label = ui.sidebarCollapsed ? "展开左侧控制台" : "收起左侧控制台";
button.setAttribute("aria-label", label);
button.title = label;
button.innerHTML = '<i data-lucide="panel-left"></i><span class="visually-hidden">左侧控制台</span>';
}
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
window.setTimeout(() => window.dispatchEvent(new Event("resize")), 180);
}
function keepRunButtonAvailable() {
const runButton = $("#runButton");
if (runButton && !runButton.classList.contains("loading")) runButton.disabled = false;
}
function syncRuntimePresentation(connectionState = window.CognitiveAgentNetwork?.getState?.()) {
const connectionText = $("#v2ConnectionText");
if (connectionText) {
const status = connectionState?.status || "connecting";
const labels = {
connecting: "正在连接服务",
online: "服务已连接",
degraded: "连接不稳定",
offline: "服务已断开",
};
connectionText.textContent = labels[status] || labels.connecting;
connectionText.title = connectionState?.lastError || connectionText.textContent;
$(".v2-sidebar-footer")?.classList.toggle(
"offline",
status === "offline" || status === "degraded",
);
}
keepRunButtonAvailable();
}
function bindEvents() {
$$('.v2-module-tab').forEach((button) => {
button.addEventListener("click", () => {
if (!button.disabled) setModule(button.dataset.module);
});
});
$$(".v2-page-button").forEach((button) => {
button.addEventListener("click", () => setPage(button.dataset.v2PageTarget));
});
$("#v2LogoutButton")?.addEventListener(
"click",
() => window.CognitiveAgentConsole?.logout?.(),
);
$("#v2SidebarToggle")?.addEventListener(
"click",
() => setSidebarCollapsed(!ui.sidebarCollapsed),
);
document.addEventListener("click", () => {
closeReasoningPicker();
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
closeReasoningPicker();
clearTwinDevicePreview({ force: true });
}
});
window.addEventListener("resize", updateScrollCues);
window.addEventListener("cognitive-agent:result", () => requestAnimationFrame(updateScrollCues));
window.addEventListener("cognitive-agent:connection", (event) => {
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());
[$("#hardwareMode"), $("#simulationMode")]
.filter(Boolean)
.forEach((node) => observer.observe(node, {
attributes: true,
childList: true,
subtree: true,
characterData: true,
}));
}
function init() {
const workspace = $(".workspace");
const commandPanel = $(".command-panel");
const mainPanel = $(".main-panel");
const originalForm = $("#requestForm");
if (!workspace || !commandPanel || !mainPanel || !originalForm) return;
document.body.classList.add("console-v2");
const panelHeading = $(".panel-heading", commandPanel);
const modeRail = $(".mode-rail", commandPanel);
const requestHeader = $(".request-header", mainPanel);
const gestureDrawer = $("#gestureDrawer");
const detached = {
panelHeading,
modeRail,
requestHeader,
gestureDrawer,
originalForm,
};
Object.values(detached).filter(Boolean).forEach((node) => node.remove());
buildSidebar(commandPanel);
prepareRuntimeControls(originalForm);
buildOperation(
mainPanel,
originalForm,
panelHeading,
requestHeader,
gestureDrawer,
);
buildModuleSwitcher(mainPanel);
buildOutput(mainPanel, modeRail);
buildSyncControlLayer();
bindEvents();
setControlMode(ui.controlMode, { triggerGesture: false });
setModule(ui.activeModule, { persist: false });
setSidebarCollapsed(ui.sidebarCollapsed);
setPage(0);
syncRuntimePresentation();
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
requestAnimationFrame(() => {
document.body.classList.add("v2-ready");
window.setTimeout(() => $("#v2BootScreen")?.remove(), 220);
});
}
window.CognitiveAgentConsoleV2 = {
getModule: () => ui.activeModule,
getRole: () => "user",
getParsedCanCommands: (result) => parsedCanCommands(result).map((item) => ({ ...item })),
positionTwinDevicePopover,
setModule,
};
document.addEventListener("DOMContentLoaded", init);
}());