Files

3084 lines
137 KiB
JavaScript
Raw Permalink 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: "" };
let pendingSpeechTiming = null;
let commandTimingState = {
inputMode: "text",
utterance: "",
requestId: "",
requestStartedAt: null,
validatedAt: null,
displayedAt: null,
renderDurationMs: null,
speechStartedAt: null,
speechResultAt: null,
speechTimings: null,
};
function resetValidatedCommandContext() {
validatedCommandsVisible = false;
validatedCommandContext = { requestId: "", utterance: "" };
}
function commandCorrelationIds(result = {}) {
return [...new Set([
result.preview_request_id,
result.supplement_of,
result.request_id,
].map((value) => String(value || "").trim()).filter(Boolean))];
}
function rememberValidatedCommandContext(result = {}) {
const ids = commandCorrelationIds(result);
validatedCommandContext = {
requestId: ids[0] || "",
utterance: String(result.utterance || "").trim(),
};
}
function validatedCommandsBelongToTask(task = {}) {
if (!validatedCommandsVisible) return false;
const result = task.result && typeof task.result === "object" ? task.result : {};
const requestIds = commandCorrelationIds(result);
const utterance = String(task.utterance || result.utterance || "").trim();
if (validatedCommandContext.requestId && requestIds.length
&& !requestIds.includes(validatedCommandContext.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;
};
const monotonicNow = () => (
typeof performance !== "undefined" && typeof performance.now === "function"
? performance.now()
: Date.now()
);
const finiteMilliseconds = (value) => {
const numeric = Number(value);
return Number.isFinite(numeric) && numeric >= 0 ? numeric : null;
};
const semanticValue = (value) => {
if (value == null) return "null";
if (["string", "number", "boolean"].includes(typeof value)) return String(value);
try {
return JSON.stringify(value);
} catch (_error) {
return String(value);
}
};
function semanticControlInstruction(operation = {}) {
const capability = String(operation.capability || "unknown");
const rawTarget = operation.target || {};
const target = rawTarget.zone || rawTarget.instance || rawTarget.device;
const argumentsList = [];
if (target != null && String(target).trim()) {
argumentsList.push(`target=${String(target).replaceAll("_", "-")}`);
}
Object.entries(operation.parameters || {})
.sort(([left], [right]) => left.localeCompare(right))
.forEach(([key, value]) => {
argumentsList.push(`${key}=${semanticValue(value)}`);
});
return `${capability}(${argumentsList.join(", ")})`;
}
function commandPlanEntries(result = {}) {
const plans = result.command_plans && typeof result.command_plans === "object"
? Object.entries(result.command_plans)
: [];
if (!plans.length && result.command_plan) plans.push(["default", result.command_plan]);
return plans;
}
function compiledCommandsForOperation(result, configKey, operation = {}) {
const operationId = String(operation.id || "");
if (!operationId) return [];
const plans = commandPlanEntries(result);
const preferred = plans.filter(([key]) => key === configKey);
const operationRefs = (command) => {
const raw = command?.operation_ref;
const refs = Array.isArray(raw) ? raw : String(raw || "").split(",");
return refs.map((ref) => String(ref).trim()).filter(Boolean);
};
const matches = (entries) => entries.flatMap(([planKey, plan]) => (
(plan?.spec?.commands || [])
.filter((command) => operationRefs(command).includes(operationId))
.map((command) => ({ command, plan, planKey }))
));
if (preferred.length) return matches(preferred);
const configurations = result.function_configs && typeof result.function_configs === "object"
? Object.entries(result.function_configs)
: result.function_config ? [["default", result.function_config]] : [];
return configurations.length === 1 && plans.length === 1 ? matches(plans) : [];
}
function compiledCommandPresentation(entries, fallback = {}) {
if (!entries.length) return fallback;
const actualCommands = [];
const frames = [];
let channel = fallback.channel || "CAN1";
entries.forEach(({ command, plan }) => {
const signals = command?.signals || {};
const message = String(command?.message || "");
const planDomain = String(plan?.spec?.domain || "");
const bus = String(command?.bus || "").toLowerCase();
const slot = boundedInteger(signals.slot, 1, 248);
if (message === "infrared.emit" || planDomain === "self_extension") {
channel = "IR";
} else if (bus.includes("seat") || bus.includes("can0")) {
channel = "CAN0";
} else if (bus.includes("body") || bus.includes("can1")) {
channel = "CAN1";
}
const actual = String(signals.debug_command || "").trim()
|| (message === "infrared.emit" && slot !== null ? `infrared emit ${slot}` : "")
|| String(fallback.command || message).trim();
if (actual && !actualCommands.includes(actual)) actualCommands.push(actual);
const frameId = String(signals.frame_id || "").trim();
const dataHex = String(signals.data_hex || "").trim();
if (frameId && dataHex) {
frames.push({ label: `${channel} 帧`, value: `${frameId} · ${dataHex}` });
} else if (message === "infrared.emit" && slot !== null) {
frames.push({
label: "GSCU1B TCP",
value: JSON.stringify({ action: "emit", data: { no: slot }, type: "infrared" }),
});
}
});
const uniqueFrames = frames.filter((frame, index) => (
frames.findIndex((candidate) => (
candidate.label === frame.label && candidate.value === frame.value
)) === index
));
return {
...fallback,
channel,
command: actualCommands.join(";") || fallback.command || "",
...(uniqueFrames.length ? {
frame: uniqueFrames[0].value,
frameLabel: uniqueFrames[0].label,
frames: uniqueFrames,
} : {}),
};
}
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}`);
}
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 ?? "");
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"]),
legrest: new Set(["up", "down", "forward", "backward", "stop"]),
};
if (validDirections[axis]?.has(direction)) {
const axisCategory = {
horizontal: "主驾座椅前后调节",
height: "主驾座椅坐盆高度调节",
basin: "主驾座椅坐盆调节",
backrest: "主驾座椅靠背调节",
legrest: "主驾座椅腿托调节",
}[axis] || "主驾座椅调节";
if (direction === "stop") return command(axisCategory, `seat driver ${axis} stop`);
const duration = boundedInteger(parameters.duration_ms, 100, 2000);
if (duration !== null) return command(axisCategory, `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 === "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)) {
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 abortedStates = new Set(["aborted", "cancelled", "canceled", "skipped"]);
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 executionStatus = String(execution?.status || "").toLowerCase();
const verificationStatus = String(telemetry.verification_status || "").toLowerCase();
const failureMessage = String(execution?.message || "").trim();
const aborted = abortedStates.has(executionStatus);
const explicitlyBlocked = executionStatus === "blocked"
|| verificationStatus === "blocked"
|| Boolean(telemetry.authorization_reason);
// Closed-loop evidence is authoritative. Some MQTT window executors
// retain a legacy `failed` operation status even after fresh 0x388
// feedback confirms the requested target; do not render that as a
// failed dispatch once the physical result is verified.
const verified = !aborted && (
verificationStatus === "verified_success"
|| telemetry.physical_effect_confirmed === true
|| telemetry.feedback_match === true
);
const executionFailed = !verified && (
executionStatus === "failed"
|| explicitlyBlocked
|| verificationStatus === "failed_or_blocked"
);
const missingWindowFeedback = /0x388/i.test(failureMessage) && (
/no fresh valid/i.test(failureMessage)
|| /未收到.*(?:有效|新鲜).*车窗位置/.test(failureMessage)
);
const missingShadeFeedback = /0x41a/i.test(failureMessage) && (
/no fresh valid/i.test(failureMessage)
|| /未收到.*(?:有效|新鲜).*遮阳帘.*位置/.test(failureMessage)
);
const safetyBlocked = executionFailed && !submitted && (
explicitlyBlocked
|| missingWindowFeedback
|| missingShadeFeedback
|| /refusing (?:blind )?motion/i.test(failureMessage)
|| /拒绝(?:启动机构|盲动|执行)/.test(failureMessage)
|| /thermal protection|热保护/i.test(failureMessage)
);
const dispatchFailed = executionFailed && !submitted && !safetyBlocked;
const deliveryState = aborted
? "aborted"
: safetyBlocked
? "blocked"
: executionFailed
? "failed"
: submitted
? "submitted"
: "not_submitted";
const deliveryLabel = aborted
? "已中止"
: safetyBlocked
? "安全阻止"
: dispatchFailed
? "下发失败"
: executionFailed
? "执行失败"
: verified
? "执行已确认"
: submitted
? "已下发 · 待状态确认"
: "未下发";
const deliveryDetail = missingWindowFeedback
? "未收到车窗 CAN1 / 0x388 实时位置,系统已阻止车窗盲动"
: missingShadeFeedback
? "未收到遮阳帘 CAN1 / 0x41A 实时位置,无法判断移动方向,系统已阻止盲动"
: failureMessage;
const transportLabel = capability === "infrared.emit"
? "红外 TCP"
: result.control_transport === "wireless_mqtt" ? "无线 CAN" : "有线 CAN";
return {
deliveryState,
deliveryLabel,
deliveryTransport: transportLabel,
...(deliveryDetail ? { deliveryDetail } : {}),
};
};
const commands = [];
const validatedControlCommands = Array.isArray(result.validated_control_commands)
? result.validated_control_commands
: [];
if (validatedControlCommands.length) {
validatedControlCommands.forEach((presentation) => {
const operation = presentation?.operation || {};
const fallback = parsedCanCommandForOperation(operation) || {
channel: presentation?.domain === "self_extension" ? "IR" : "CAN1",
category: String(operation.capability || "控制指令"),
command: "",
...(operation.parse_source ? { parseSource: String(operation.parse_source) } : {}),
};
const channel = new Set(["CAN0", "CAN1", "IR"]).has(presentation?.channel)
? presentation.channel
: fallback.channel;
const frames = (Array.isArray(presentation?.frames) ? presentation.frames : [])
.map((frame) => ({
label: String(frame?.label || "控制帧"),
value: String(frame?.value || ""),
}))
.filter((frame) => frame.value);
const command = String(presentation?.actual_command || fallback.command || "").trim();
if (!command) return;
const delivery = deliveryForOperation(operation);
const explicitDeliveryState = String(presentation?.delivery_state || "").trim();
const explicitDeliveryLabel = String(presentation?.delivery_label || "").trim();
const explicitDeliveryDetail = String(presentation?.delivery_detail || "").trim();
const explicitDeliveryTransport = String(presentation?.delivery_transport || "").trim();
commands.push({
...fallback,
channel,
command,
category: String(presentation?.category || fallback.category || "控制指令"),
...(frames.length ? {
frame: frames[0].value,
frameLabel: frames[0].label,
frames,
} : {}),
semantic: String(
presentation?.semantic_instruction || semanticControlInstruction(operation),
),
operationRef: String(operation.id || ""),
...delivery,
...(explicitDeliveryState ? { deliveryState: explicitDeliveryState } : {}),
...(explicitDeliveryLabel ? { deliveryLabel: explicitDeliveryLabel } : {}),
...(explicitDeliveryDetail ? { deliveryDetail: explicitDeliveryDetail } : {}),
...(explicitDeliveryTransport ? {
deliveryTransport: explicitDeliveryTransport,
} : {}),
});
});
} else {
configurationEntries.forEach(([configKey, configuration]) => {
(configuration?.spec?.operations || []).forEach((operation) => {
const fallback = parsedCanCommandForOperation(operation) || {
channel: configuration?.spec?.domain === "self_extension" ? "IR" : "CAN1",
category: String(operation.capability || "控制指令"),
command: "",
...(operation.parse_source ? { parseSource: String(operation.parse_source) } : {}),
};
const compiled = compiledCommandsForOperation(result, configKey, operation);
const item = compiledCommandPresentation(compiled, fallback);
if (item.command) {
commands.push({
...item,
semantic: semanticControlInstruction(operation),
operationRef: String(operation.id || ""),
...deliveryForOperation(operation),
});
}
});
});
}
const unique = new Map();
commands.forEach((item) => {
const key = `${item.operationRef || item.semantic}:${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: "语音语义",
vehicle_mqtt_extension: "车端 MQTT",
}[source] || "云端模型";
}
const commandTraceStageLabels = {
intent_parse: "意图解析",
context_resolve: "上下文解析",
domain_route: "控制域路由",
function_generate: "语义指令生成",
schema_validate: "Schema 校验",
domain_validate: "领域语义校验",
compile: "实际指令编译",
nfc_peripheral_route: "红外能力路由",
local_peripheral_plan: "红外本地规则编译",
nfc_peripheral_sequence: "红外指令序列生成",
};
const speechTimingLabels = {
audio_read_ms: "音频读取与重采样",
vad_ms: "语音活动检测",
asr_ms: "语音识别 ASR",
assembly_ms: "分段文本组装",
semantic_ms: "语音语义处理",
command_resolution_ms: "本地指令消歧",
recovery_ms: "语音识别恢复",
total_ms: "语音服务总耗时",
};
function parsedCommandTimingRows(result = {}, timingState = {}) {
const rows = [];
const push = (group, key, label, value) => {
const durationMs = finiteMilliseconds(value);
if (durationMs !== null) rows.push({ group, key, label, durationMs });
};
if (timingState.inputMode === "speech") {
Object.entries(speechTimingLabels).forEach(([key, label]) => {
push("speech", key, label, timingState.speechTimings?.[key]);
});
if (timingState.speechStartedAt != null && timingState.speechResultAt != null) {
push(
"speech",
"speech_capture_to_text",
"语音开始 → 文本就绪",
timingState.speechResultAt - timingState.speechStartedAt,
);
}
}
const traceEvents = Array.isArray(result.trace_events) ? result.trace_events : [];
const compileIndex = traceEvents.map((event) => event?.stage).lastIndexOf("compile");
const validatedTraceEvents = compileIndex >= 0
? traceEvents.slice(0, compileIndex + 1)
: traceEvents;
const localRuleResult = String(result.provider_used || "").startsWith("local")
|| parsedCanCommands(result).some((item) => item.parseSource === "local_rule");
const localBudgetMs = finiteMilliseconds(result.local_control_budget_ms);
const localWithinBudget = result.local_control_within_budget === true;
push(
"backend",
"local_control_elapsed",
localBudgetMs === null
? "本地规则解析与下发"
: `本地规则解析与下发(${localWithinBudget ? "1 秒内" : "超出 1 秒"})`,
result.local_control_elapsed_ms,
);
validatedTraceEvents.forEach((event, index) => {
const stage = String(event?.stage || "");
if (!(stage in commandTraceStageLabels)) return;
const label = stage === "intent_parse" && localRuleResult
? "本地规则意图解析"
: commandTraceStageLabels[stage];
push("backend", `${stage}-${index}`, label, event.duration_ms);
});
if (timingState.requestStartedAt != null && timingState.validatedAt != null) {
push(
"frontend",
"request_to_validated",
"请求提交 → 编译结果到达",
timingState.validatedAt - timingState.requestStartedAt,
);
}
push("frontend", "dom_render", "前端指令渲染", timingState.renderDurationMs);
if (timingState.requestStartedAt != null && timingState.displayedAt != null) {
push(
"total",
"request_to_display",
"文本提交 → 指令显示",
timingState.displayedAt - timingState.requestStartedAt,
);
}
if (timingState.inputMode === "speech"
&& timingState.speechStartedAt != null
&& timingState.displayedAt != null) {
push(
"total",
"speech_to_display",
"语音开始 → 指令显示",
timingState.displayedAt - timingState.speechStartedAt,
);
}
return rows;
}
const formattedDuration = (value) => {
const duration = finiteMilliseconds(value);
if (duration === null) return "--";
if (duration > 0 && duration < 0.1) return "<0.1 ms";
return `${duration < 10 ? duration.toFixed(1) : Math.round(duration)} ms`;
};
function renderCommandTimings(result = null, { pending = false } = {}) {
const container = $("#v2CommandTiming");
if (!container) return;
const rows = result ? parsedCommandTimingRows(result, commandTimingState) : [];
if (!rows.length && !pending) {
container.hidden = true;
container.innerHTML = "";
return;
}
container.hidden = false;
if (!rows.length) {
container.innerHTML = `<p>阶段耗时计时中 · ${commandTimingState.inputMode === "speech" ? "语音输入" : "普通文本"}</p>`;
return;
}
const total = [...rows].reverse().find((row) => row.group === "total");
container.innerHTML = `
<header>
<strong>阶段耗时</strong>
<span>${commandTimingState.inputMode === "speech" ? "语音输入" : "普通文本"}</span>
${total ? `<b>${escapeHtml(formattedDuration(total.durationMs))}</b>` : ""}
</header>
<ol>${rows.map((row) => `
<li data-timing-group="${escapeHtml(row.group)}" data-timing-stage="${escapeHtml(row.key)}">
<span>${escapeHtml(row.label)}</span><b>${escapeHtml(formattedDuration(row.durationMs))}</b>
</li>`).join("")}</ol>`;
}
function beginCommandTiming(request = {}) {
const input = $("#utterance");
const utterance = String(request.utterance ?? input?.value ?? "").trim();
const speechUtteranceId = String(
request.speech_utterance_id ?? input?.dataset?.speechUtteranceId ?? "",
);
const speechValue = String(
request.speech_value ?? input?.dataset?.speechValue ?? "",
);
const speechMatch = Boolean(
speechUtteranceId
&& pendingSpeechTiming?.utteranceId === speechUtteranceId
&& speechValue === utterance,
);
commandTimingState = {
inputMode: speechMatch ? "speech" : "text",
utterance,
requestId: "",
requestStartedAt: monotonicNow(),
validatedAt: null,
displayedAt: null,
renderDurationMs: null,
speechStartedAt: speechMatch ? pendingSpeechTiming.startedAt : null,
speechResultAt: speechMatch ? pendingSpeechTiming.resultAt : null,
speechTimings: speechMatch ? { ...(pendingSpeechTiming.timings || {}) } : null,
};
}
function commandResultMatchesTimedRequest(result = {}) {
if (commandTimingState.requestStartedAt == null) return true;
const expectedUtterance = String(commandTimingState.utterance || "")
.replace(/\s+/g, "")
.trim();
const receivedUtterance = String(result.utterance || "")
.replace(/\s+/g, "")
.trim();
if (expectedUtterance && receivedUtterance
&& expectedUtterance !== receivedUtterance
&& !expectedUtterance.includes(receivedUtterance)
&& !receivedUtterance.includes(expectedUtterance)) return false;
const requestIds = commandCorrelationIds(result);
if (commandTimingState.requestId && requestIds.length
&& !requestIds.includes(commandTimingState.requestId)) return false;
if (!commandTimingState.requestId && requestIds.length) {
[commandTimingState.requestId] = requestIds;
}
return true;
}
function renderParsedCanCommands(
result,
{ message = "", pending = false, recordTiming = false, reveal = false } = {},
) {
const renderStartedAt = monotonicNow();
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, item.deliveryDetail].filter(Boolean).join(" · "))}">${escapeHtml(item.deliveryLabel)}</b>
<i class="v2-can-command-category">${escapeHtml(item.category)}</i>
</span>
${item.deliveryDetail && ["blocked", "failed", "aborted"].includes(item.deliveryState) ? `
<small class="v2-can-command-delivery-detail">
<b>状态说明</b>${escapeHtml(item.deliveryDetail)}
</small>` : ""}
<div class="v2-can-command-layer v2-can-command-semantic">
<small>语义控制指令</small>
<code>${escapeHtml(item.semantic)}</code>
</div>
<div class="v2-can-command-layer v2-can-command-actual">
<small>实际控制指令</small>
<code>${escapeHtml(item.command)}</code>
</div>
${(item.frames || (item.frame ? [{ label: item.frameLabel || "控制帧", value: item.frame }] : [])).map((frame) => `<small class="v2-can-command-frame"><b>${escapeHtml(frame.label)}</b> · ${escapeHtml(frame.value)}</small>`).join("")}
</li>`).join("")}</ol>`;
} else {
list.innerHTML = `<p>${escapeHtml(message || "本次请求未解析出控制指令")}</p>`;
}
if (reveal && commands.length) {
panel.open = true;
if (ui.sidebarCollapsed) setSidebarCollapsed(false);
}
if (recordTiming && commands.length) {
if (commandTimingState.validatedAt == null) commandTimingState.validatedAt = renderStartedAt;
if (commandTimingState.displayedAt == null) {
commandTimingState.displayedAt = monotonicNow();
commandTimingState.renderDurationMs = commandTimingState.displayedAt - renderStartedAt;
}
}
renderCommandTimings(result, { pending });
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 v2-control-link-card", `
<span class="v2-live-dot"></span>
<div><strong>智舱控制链路</strong><small id="v2ConnectionText" aria-live="polite">正在连接服务</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, modeRail) {
const switcher = make("nav", "v2-module-switcher", "");
switcher.id = "v2ModuleSwitcher";
switcher.dataset.unifiedSync = "true";
switcher.setAttribute("aria-label", "页面板块展开与收起");
switcher.innerHTML = `
<nav class="v2-panel-control-dock" id="v2PanelControlDock" aria-label="页面板块展开与收起"></nav>`;
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);
}
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: "",
deviceRenderDirty: true,
activeDevice: null,
pinnedDevice: null,
previewClearTimer: null,
hoverSuppressedUntil: 0,
controlDevice: null,
deviceDrafts: {},
pendingControls: {},
logs: [],
logKeys: new Set(),
extensionFingerprint: "",
};
const extensionControlSync = {
revision: -1,
requestId: "",
running: false,
stopped: true,
controller: null,
};
const nonDispatchTaskSources = new Set(["mode_editor_compile"]);
function isDispatchTask(task = {}) {
return !nonDispatchTaskSources.has(String(task.source || "").trim());
}
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", "关闭"], ["low", "1 档(低)"], ["high", "3 档(高)"],
];
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 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');
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 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", "开关状态", ambientPowerState() === "off" ? "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({ force = false } = {}) {
const grid = $("#v2DeviceSyncGrid");
if (!grid) return false;
if (!force && !panelVisible("devices")) {
displaySync.deviceRenderDirty = true;
return false;
}
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)]));
displaySync.deviceRenderDirty = false;
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
return 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 (!panelVisible("devices")) {
displaySync.deviceRenderDirty = true;
return;
}
if (displaySync.deviceFrame) return;
displaySync.deviceFrame = requestAnimationFrame(() => {
displaySync.deviceFrame = null;
const fingerprint = JSON.stringify(syncedDevices.map((device) => [device.id, deviceReadoutRows(device)]));
if (!displaySync.deviceRenderDirty
&& 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 count = $("#v2SyncLogCount");
if (count) {
count.textContent = String(items.length);
count.title = `${items.length} 条记录`;
}
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 setSyncLogCollapsed(collapsed) {
const panel = $(".v2-sync-log");
const list = $("#v2SyncLogList");
const toggle = $("#v2SyncLogToggle");
if (!panel || !list || !toggle) return;
const nextCollapsed = Boolean(collapsed);
const label = nextCollapsed ? "展开运行日志与同步记录" : "折叠运行日志与同步记录";
panel.dataset.collapsed = String(nextCollapsed);
list.hidden = nextCollapsed;
toggle.setAttribute("aria-expanded", String(!nextCollapsed));
toggle.setAttribute("aria-label", label);
toggle.title = label;
requestAnimationFrame(syncSidebarLogBaseline);
}
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";
}
if (key === "devices" && visible) renderSyncedDevices({ force: true });
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
window.dispatchEvent(new CustomEvent("cognitive-agent:side-panel-visibility", {
detail: { panel: key, visible: Boolean(visible) },
}));
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 extensionControlDelivery(action = {}) {
const state = String(action.state || "pending");
const defaults = {
pending: ["not_submitted", "等待下发"],
executing: ["executing", "正在下发"],
submitted_unverified: ["submitted", "已下发 · 物理效果未确认"],
verified_success: ["submitted", "执行已确认"],
failed: ["failed", "下发失败"],
aborted: ["aborted", "已中止"],
};
const [deliveryState, deliveryLabel] = defaults[state] || defaults.pending;
return {
deliveryState,
deliveryLabel: String(action.state_label || deliveryLabel),
deliveryDetail: String(action.state_detail || ""),
};
}
function extensionControlResult(snapshot = {}) {
const requestId = String(snapshot.last_request_id || "");
const actions = Array.isArray(snapshot.action_statuses)
? snapshot.action_statuses
: [];
return {
request_id: requestId,
utterance: "车端 MQTT 外设控制",
status: snapshot.state === "failed" ? "failed" : "succeeded",
execution_mode: "hardware",
control_transport: "extension_mqtt",
orchestration_route: "vehicle_mqtt_extension_control",
validated_control_commands: actions.map((action, position) => {
const index = Number.isInteger(Number(action.index))
? Number(action.index)
: position;
const atMs = Math.max(0, Number(action.at_ms) || 0);
const slot = Number(action.slot);
const extensionName = String(action.extension_name || "红外外设");
const actionName = String(action.action || "红外动作");
const targetScope = String(action.target_scope || "external-device");
const delivery = extensionControlDelivery(action);
return {
domain: "self_extension",
channel: "IR",
category: `${extensionName} · target_scope=${targetScope} · at_ms=${atMs}`,
semantic_instruction: `${extensionName} · ${actionName}`,
actual_command: `infrared emit ${slot}`,
delivery_state: delivery.deliveryState,
delivery_label: delivery.deliveryLabel,
delivery_detail: [delivery.deliveryDetail, `at_ms=${atMs}`]
.filter(Boolean)
.join(" · "),
delivery_transport: "车端 MQTT → 红外 TCP",
frames: [{
label: "GSCU1B TCP",
value: JSON.stringify({ action: "emit", data: { no: slot }, type: "infrared" }),
}],
operation: {
id: `${requestId || "extension-ctl"}:${index}`,
domain: "self_extension",
capability: "infrared.emit",
target: { device: targetScope, name: extensionName },
parameters: { action: actionName, slot },
parse_source: "vehicle_mqtt_extension",
},
};
}),
};
}
function renderExtensionControlRejection(snapshot = {}) {
const panel = $("#v2CanCommandPanel");
const list = $("#v2CanCommandList");
const count = $("#v2CanCommandCount");
if (!panel || !list || !count) return;
count.textContent = "0";
panel.dataset.state = "error";
panel.open = true;
list.innerHTML = `<p>车端 MQTT 外设控制已拒绝:${escapeHtml(snapshot.last_error || "消息未通过校验")}</p>`;
if (ui.sidebarCollapsed) setSidebarCollapsed(false);
requestAnimationFrame(syncSidebarLogBaseline);
}
function applyExtensionControlSnapshot(snapshot = {}) {
const revision = Number(snapshot.revision);
if (!Number.isInteger(revision)) return;
if (revision < extensionControlSync.revision) {
extensionControlSync.revision = -1;
extensionControlSync.requestId = "";
}
if (revision <= extensionControlSync.revision) return;
extensionControlSync.revision = revision;
const requestId = String(snapshot.last_request_id || "");
const state = String(snapshot.state || "");
const actions = Array.isArray(snapshot.action_statuses)
? snapshot.action_statuses
: [];
if (requestId) extensionControlSync.requestId = requestId;
if (actions.length) {
renderParsedCanCommands(extensionControlResult(snapshot), { reveal: true });
appendSyncLog({
key: `extension-control:${requestId}:${state}:${snapshot.executed_action_count || 0}`,
category: "extension",
title: state === "completed"
? "车端 MQTT 外设控制已下发"
: state === "failed"
? "车端 MQTT 外设控制下发失败"
: "正在执行车端 MQTT 外设控制",
detail: `${actions.length} 条红外指令 · ${snapshot.last_error || state}`,
status: state === "failed" ? "error" : state === "completed" ? "success" : "info",
});
return;
}
if (requestId && ["rejected", "queue_full"].includes(state)) {
renderExtensionControlRejection(snapshot);
appendSyncLog({
key: `extension-control:${requestId}:${state}`,
category: "extension",
title: "车端 MQTT 外设控制已拒绝",
detail: String(snapshot.last_error || "消息未通过校验"),
status: "error",
});
}
}
const extensionControlRetryDelay = () => new Promise((resolve) => {
window.setTimeout(resolve, 1000);
});
async function monitorExtensionControl() {
if (extensionControlSync.running) return;
extensionControlSync.running = true;
extensionControlSync.stopped = false;
try {
while (!extensionControlSync.stopped) {
const controller = new AbortController();
extensionControlSync.controller = controller;
const query = new URLSearchParams({
after_revision: String(extensionControlSync.revision),
timeout_ms: "25000",
});
try {
const response = await fetch(`/api/extension/control/status?${query}`, {
credentials: "same-origin",
cache: "no-store",
headers: { Accept: "application/json" },
signal: controller.signal,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
applyExtensionControlSnapshot(await response.json());
} catch (error) {
if (extensionControlSync.stopped || error?.name === "AbortError") break;
console.warn("[extension-control-sync]", error);
await extensionControlRetryDelay();
} finally {
if (extensionControlSync.controller === controller) {
extensionControlSync.controller = null;
}
}
}
} finally {
extensionControlSync.running = false;
}
}
function startExtensionControlMonitor() {
if (extensionControlSync.running) return;
extensionControlSync.stopped = false;
void monitorExtensionControl();
}
function stopExtensionControlMonitor() {
extensionControlSync.stopped = true;
extensionControlSync.controller?.abort();
extensionControlSync.controller = null;
}
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 || {};
if (isDispatchTask(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>
<section class="v2-sync-log" aria-labelledby="v2SyncLogTitle" data-collapsed="true">
<header>
<div><h3 id="v2SyncLogTitle">运行日志 / 同步记录</h3></div>
<div class="v2-sync-log-actions">
<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>
<button type="button" class="v2-sync-log-toggle" id="v2SyncLogToggle" aria-controls="v2SyncLogList" aria-expanded="false" aria-label="展开运行日志与同步记录" title="展开运行日志与同步记录"><span id="v2SyncLogCount">0</span><i data-lucide="chevron-down"></i></button>
</div>
</header>
<ol id="v2SyncLogList" aria-live="polite" hidden></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>
<section class="v2-command-timing" id="v2CommandTiming" aria-live="polite" hidden></section>
<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");
window.addEventListener(
"cognitive-agent:vehicle-state-rendered",
scheduleSyncedDeviceRender,
);
}
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);
$("#v2SyncLogToggle")?.addEventListener("click", () => {
setSyncLogCollapsed($(".v2-sync-log")?.dataset.collapsed !== "true");
});
setSyncLogCollapsed(true);
const beginPendingControlRequest = (request = {}) => {
beginCommandTiming(request);
resetValidatedCommandContext();
renderParsedCanCommands(null, {
message: "正在解析自然语言并生成控制指令…",
pending: true,
});
};
$("#requestForm")?.addEventListener("submit", () => {
beginPendingControlRequest();
}, true);
window.addEventListener("cognitive-agent:control-request-start", (event) => {
beginPendingControlRequest(event.detail || {});
});
window.addEventListener("cognitive-agent:speech-state", (event) => {
const phase = event.detail?.phase;
if (phase === "capturing" && !pendingSpeechTiming?.captureActive) {
pendingSpeechTiming = {
utteranceId: "",
startedAt: monotonicNow(),
resultAt: null,
timings: null,
captureActive: true,
};
} else if (["finalizing", "success", "error"].includes(phase)
&& pendingSpeechTiming) {
pendingSpeechTiming.captureActive = false;
}
});
window.addEventListener("cognitive-agent:speech-result", (event) => {
const result = event.detail || {};
pendingSpeechTiming = {
...(pendingSpeechTiming || { startedAt: null }),
utteranceId: String(result.utterance_id || ""),
resultAt: monotonicNow(),
timings: result.timings && typeof result.timings === "object"
? { ...result.timings }
: null,
captureActive: false,
};
});
window.addEventListener("cognitive-agent:validated-can-commands", (event) => {
const result = event.detail || {};
if (!commandResultMatchesTimedRequest(result)) return;
validatedCommandsVisible = parsedCanCommands(result).length > 0;
if (validatedCommandsVisible) rememberValidatedCommandContext(result);
else resetValidatedCommandContext();
renderParsedCanCommands(result, {
message: "控制指令已解析,正在等待传输结果…",
recordTiming: true,
reveal: true,
});
});
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);
const matchesTimedRequest = commandResultMatchesTimedRequest(result);
if (matchesTimedRequest && commands.length) {
validatedCommandsVisible = true;
rememberValidatedCommandContext(result);
renderParsedCanCommands(result, {
recordTiming: commandTimingState.displayedAt == null,
reveal: commandTimingState.requestStartedAt != null,
});
} else if (matchesTimedRequest && !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) => {
const completedUtterance = String(event.detail?.utterance || "")
.replace(/\s+/g, "")
.trim();
const expectedUtterance = String(commandTimingState.utterance || "")
.replace(/\s+/g, "")
.trim();
const matchesTimedRequest = !expectedUtterance || !completedUtterance
|| expectedUtterance === completedUtterance;
if (matchesTimedRequest && !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);
startExtensionControlMonitor();
window.addEventListener("pagehide", stopExtensionControlMonitor);
window.addEventListener("pageshow", startExtensionControlMonitor);
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 selectedTransport = $('.vehicle-transport-options [data-control-transport][aria-pressed="true"]')
|| $('.vehicle-transport-options [data-control-transport].active');
const transport = selectedTransport?.dataset.controlTransport || "";
const transportOnline = selectedTransport?.dataset.connectionState === "online";
const transportLabel = transport === "wireless_mqtt" ? "无线 CAN" : "有线 CAN";
const footer = connectionText.closest(".v2-control-link-card");
let presentationState = status;
let label = "正在连接服务";
if (status === "online" && transport) {
presentationState = transportOnline ? "online" : "offline";
label = transport === "wireless_mqtt"
? `${transportLabel} · MQTT ${transportOnline ? "已连接" : "未连接"}`
: `${transportLabel} · 信号链路${transportOnline ? "正常" : "断开"}`;
} else if (status === "online") {
presentationState = "online";
label = "服务已连接";
} else if (status === "degraded") {
label = transport ? `${transportLabel} · 主服务连接不稳定` : "主服务连接不稳定";
} else if (status === "offline") {
label = transport ? `${transportLabel} · 主服务已断开` : "主服务已断开";
}
connectionText.textContent = label;
connectionText.title = connectionState?.lastError
|| (!transportOnline ? selectedTransport?.title : "")
|| label;
if (footer) {
footer.dataset.transport = transport || "service";
footer.dataset.state = presentationState;
footer.classList.toggle("offline", presentationState !== "online");
}
}
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", () => {
syncRuntimePresentation();
scheduleSyncedDeviceRender();
if (panelVisible("devices") && displaySync.activeDevice) {
presentTwinDevice(displaySync.activeDevice, {
pinned: Boolean(displaySync.pinnedDevice),
selectSeat: false,
});
}
});
const transportOptions = $(".vehicle-transport-options");
if (transportOptions) {
new MutationObserver(() => syncRuntimePresentation()).observe(transportOptions, {
attributes: true,
attributeFilter: ["aria-pressed", "class", "data-connection-state", "title"],
subtree: true,
});
}
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, modeRail);
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);
}());