merge: integrate remote MQTT routing with seat-scoped dynamic modes
This commit is contained in:
@@ -4488,27 +4488,7 @@ function renderControlTransport() {
|
||||
? runtime?.state_sync?.last_error || ""
|
||||
: `${id === "wireless_mqtt" ? "无线 MQTT" : "有线 CAN"} 当前未配置(仍可手动选择)`;
|
||||
});
|
||||
const selected = selectedVehicleTransport();
|
||||
const note = $("#vehicleTransportNote");
|
||||
renderCabinLiveStatus();
|
||||
if (!note) return;
|
||||
if (selected?.state_configured !== true) {
|
||||
note.textContent = state.controlTransport === "wireless_mqtt"
|
||||
? "已手动选择无线控制 · MQTT 当前未配置或不可用"
|
||||
: "已手动选择有线控制 · ZLG CAN 当前未配置或不可用";
|
||||
return;
|
||||
}
|
||||
if (state.controlTransport === "wireless_mqtt") {
|
||||
const connection = transportConnectionOnline("wireless_mqtt", selected) ? "已连接" : "未连接";
|
||||
note.textContent = `MQTT ${connection} · 状态 ${selected?.condition_topic || "n7/n7-headunit/can/state"} · 控制 ${selected?.control_topic || "n7/n7-headunit/can/command"}`;
|
||||
} else {
|
||||
const connection = transportConnectionOnline("wired_can", selected) ? "已连接" : "未连接";
|
||||
const dualChannel = selected?.target_policy
|
||||
=== "dual_channel_wired_can0_passenger_can1_vehicle";
|
||||
note.textContent = dualChannel
|
||||
? `ZLG 有线 ${connection} · CAN0 副驾经典 CAN 500k · CAN1 车辆 CAN FD 500k/2M`
|
||||
: `ZLG 有线 ${connection} · CH${selected?.channel ?? 1} · CAN FD 500k/2M`;
|
||||
}
|
||||
}
|
||||
|
||||
function selectControlTransport(transport) {
|
||||
@@ -6719,8 +6699,6 @@ async function loadRuntime() {
|
||||
? "真实下发已通过后端执行配置校验"
|
||||
: `可手动选择真实下发;当前不可执行:${hardwareExecutionIssue(hardware, vehicleCan)}`;
|
||||
const simulation = state.runtime.simulated_hardware || {};
|
||||
const profileText = (hardware.firmware_profiles || []).join(" / ") || "未选择 profile";
|
||||
const flashText = hardware.auto_flash_enabled ? "自动烧录已启用" : "自动烧录未启用";
|
||||
const concurrencyText = hardware.concurrency_policy === "single_flight_fail_fast"
|
||||
? " · 动作防冲突"
|
||||
: "";
|
||||
@@ -6728,23 +6706,6 @@ async function loadRuntime() {
|
||||
&& hardware.access_policy === "local_approval_idle_lease"
|
||||
? ` · 本地审批 · 空闲 ${hardware.access_inactivity_timeout_seconds || 180} 秒自动停止`
|
||||
: "";
|
||||
const vehicleRouteText = mqttSelected
|
||||
? `MQTT ${vehicleCan.host || "127.0.0.1"}:${vehicleCan.port || 1883} · ${vehicleCan.condition_topic || "n7/n7-headunit/can/state"} / ${vehicleCan.control_topic || "n7/n7-headunit/can/command"}`
|
||||
: `${vehicleCan.backend || "zlgcan"} · CH${vehicleCan.channel ?? 1} 车身 CAN FD ${Number(vehicleCan.arbitration?.bitrate || 500000) / 1000}k/${Number(vehicleCan.data?.bitrate || 2000000) / 1000000}M`;
|
||||
$("#hardwareNote").textContent = ch1DriverOnly && vehicleHardwareReady && !seatHardwareReady
|
||||
? `${vehicleRouteText} · 仅主驾/车端白名单 · CH0/T2G/IAR 已禁用${accessText}${concurrencyText}`
|
||||
: linkedHardwareReady
|
||||
? `${mqttDualChannel ? "MQTT CAN0 副驾座椅 · MQTT CAN1 其余车辆" : `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s`} · ${vehicleRouteText}${mqttDualChannel ? " · 远端自动烧录禁用" : ` · ${profileText} · ${flashText}`}${accessText}${concurrencyText}`
|
||||
: seatHardwareReady
|
||||
? `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s · ${profileText} · ${flashText} · 车端 CAN 未启用,其他领域保持虚拟执行${accessText}${concurrencyText}`
|
||||
: `可手动选择真实下发;当前不可执行:${hardwareExecutionIssue(hardware, vehicleCan)}`;
|
||||
$("#hardwareAliasNote").textContent = ch1DriverOnly && vehicleHardwareReady && !seatHardwareReady
|
||||
? `${mqttSelected ? "MQTT" : "CH1"} → 车身 CAN / 主驾座椅 · CH0 与副驾 T2G 路径禁止`
|
||||
: linkedHardwareReady
|
||||
? `${mqttDualChannel ? "MQTT CAN0" : "CH0"} → 副驾座椅控制器 · ${mqttDualChannel ? "MQTT CAN1" : "CH1"} → 其余车辆 CAN`
|
||||
: seatHardwareReady
|
||||
? `CH${hardware.channel} → 副驾座椅控制器 · 车身控制暂用虚拟 CAN`
|
||||
: "尚未配置可执行的物理 CAN 通道";
|
||||
$("#hardwareCoverage").textContent = ch1DriverOnly && vehicleHardwareReady && !seatHardwareReady
|
||||
? `${mqttSelected ? "MQTT" : "CH1"} 主驾 + 车端设备 · CAN 字段回读${accessText}${concurrencyText}`
|
||||
: linkedHardwareReady
|
||||
|
||||
@@ -1129,6 +1129,13 @@
|
||||
}
|
||||
|
||||
function applyAssistantEvent(event, message) {
|
||||
if (event.type === "validated_control_commands" && event.payload) {
|
||||
window.dispatchEvent(new CustomEvent(
|
||||
"cognitive-agent:validated-can-commands",
|
||||
{ detail: event.payload },
|
||||
));
|
||||
return;
|
||||
}
|
||||
if (event.type === "answer.delta") {
|
||||
setMessageContent(message, `${message.content}${event.delta || ""}`);
|
||||
return;
|
||||
@@ -1313,6 +1320,21 @@
|
||||
return;
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent(
|
||||
"cognitive-agent:control-request-start",
|
||||
{
|
||||
detail: {
|
||||
utterance: messageText,
|
||||
speech_utterance_id: suggestedMessage
|
||||
? ""
|
||||
: String(input?.dataset.speechUtteranceId || ""),
|
||||
speech_value: suggestedMessage
|
||||
? ""
|
||||
: String(input?.dataset.speechValue || ""),
|
||||
},
|
||||
},
|
||||
));
|
||||
|
||||
createMessage("user", messageText);
|
||||
if (!suggestedMessage) {
|
||||
input.value = "";
|
||||
|
||||
+143
-36
@@ -281,19 +281,6 @@
|
||||
box-shadow: 0 5px 13px color-mix(in srgb, var(--v2-accent) 20%, transparent);
|
||||
}
|
||||
|
||||
.console-v2 .v2-transport-field > #vehicleTransportNote {
|
||||
display: -webkit-box;
|
||||
margin: 6px 1px 0;
|
||||
overflow: hidden;
|
||||
color: #809091;
|
||||
font-size: 8px;
|
||||
line-height: 1.35;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.v2-sidebar-footer {
|
||||
min-height: 50px;
|
||||
display: grid;
|
||||
@@ -8233,28 +8220,6 @@ body.device-power-drawer-open {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field .provider-note {
|
||||
min-height: 0;
|
||||
margin: 5px 1px 0;
|
||||
overflow: hidden;
|
||||
color: #809091;
|
||||
font-size: 8px;
|
||||
line-height: 1.35;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field #hardwareNote {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field #hardwareAliasNote {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-simulation-fault-field select {
|
||||
min-height: 34px;
|
||||
padding: 0 8px;
|
||||
@@ -8336,8 +8301,83 @@ body.device-power-drawer-open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-command-timing {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 7px 9px;
|
||||
color: #557270;
|
||||
background: rgba(232, 244, 242, 0.7);
|
||||
border-top: 1px solid rgba(19, 105, 99, 0.1);
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-command-timing[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-command-timing > p {
|
||||
margin: 0;
|
||||
font-size: 8px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-command-timing > header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-command-timing > header strong {
|
||||
font-size: 9px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-command-timing > header span {
|
||||
padding: 1px 5px;
|
||||
color: #2366a1;
|
||||
background: #e4f0fb;
|
||||
border-radius: 999px;
|
||||
font-size: 7px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-command-timing > header b {
|
||||
margin-left: auto;
|
||||
color: #08766f;
|
||||
font: 850 8px/1.4 "SFMono-Regular", Consolas, monospace;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-command-timing ol {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 3px 9px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-command-timing li {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 5px;
|
||||
font-size: 7px;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-command-timing li span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-command-timing li b {
|
||||
flex: 0 0 auto;
|
||||
color: #315e5b;
|
||||
font: 800 7px/1.4 "SFMono-Regular", Consolas, monospace;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list {
|
||||
max-height: min(24dvh, 190px);
|
||||
max-height: min(34dvh, 280px);
|
||||
padding: 7px;
|
||||
overflow: auto;
|
||||
border-top: 1px solid rgba(19, 105, 99, 0.1);
|
||||
@@ -8472,6 +8512,35 @@ body.device-power-drawer-open {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-layer {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
padding: 4px 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-layer > small {
|
||||
color: #758886;
|
||||
font-size: 7px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-semantic {
|
||||
background: rgba(35, 102, 161, 0.07);
|
||||
border-left: 2px solid rgba(35, 102, 161, 0.38);
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-semantic code {
|
||||
color: #245c8d;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-actual {
|
||||
background: rgba(8, 118, 111, 0.07);
|
||||
border-left: 2px solid rgba(8, 118, 111, 0.42);
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-frame {
|
||||
display: block;
|
||||
color: #8a6b36;
|
||||
@@ -8479,6 +8548,10 @@ body.device-power-drawer-open {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-frame > b {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-transport-field > .field-label {
|
||||
color: #466664;
|
||||
font-size: 10px;
|
||||
@@ -9530,6 +9603,40 @@ body.device-power-drawer-open {
|
||||
color: var(--command-frame);
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-command-timing {
|
||||
color: var(--muted-foreground);
|
||||
background: color-mix(in oklch, var(--accent) 55%, transparent);
|
||||
border-top-color: var(--border);
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-command-timing > header span {
|
||||
color: var(--command-source);
|
||||
background: color-mix(in oklch, var(--command-source) 12%, var(--card));
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-command-timing > header b,
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-command-timing li b {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li[data-can-channel] .v2-can-command-layer > small {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li[data-can-channel] .v2-can-command-semantic {
|
||||
background: color-mix(in oklch, var(--command-source) 8%, var(--card));
|
||||
border-left-color: color-mix(in oklch, var(--command-source) 52%, var(--border));
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li[data-can-channel] .v2-can-command-semantic code {
|
||||
color: var(--command-source);
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li[data-can-channel] .v2-can-command-actual {
|
||||
background: color-mix(in oklch, var(--primary) 8%, var(--card));
|
||||
border-left-color: color-mix(in oklch, var(--primary) 52%, var(--border));
|
||||
}
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.console-v2 .v2-mode-rail > .mode-rail-bar {
|
||||
grid-template-columns: auto minmax(150px, 1fr) auto;
|
||||
|
||||
+497
-41
@@ -23,15 +23,37 @@
|
||||
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: String(result.request_id || ""),
|
||||
requestId: ids[0] || "",
|
||||
utterance: String(result.utterance || "").trim(),
|
||||
};
|
||||
}
|
||||
@@ -39,10 +61,10 @@
|
||||
function validatedCommandsBelongToTask(task = {}) {
|
||||
if (!validatedCommandsVisible) return false;
|
||||
const result = task.result && typeof task.result === "object" ? task.result : {};
|
||||
const requestId = String(result.request_id || "");
|
||||
const requestIds = commandCorrelationIds(result);
|
||||
const utterance = String(task.utterance || result.utterance || "").trim();
|
||||
if (validatedCommandContext.requestId && requestId
|
||||
&& validatedCommandContext.requestId !== requestId) return false;
|
||||
if (validatedCommandContext.requestId && requestIds.length
|
||||
&& !requestIds.includes(validatedCommandContext.requestId)) return false;
|
||||
if (validatedCommandContext.utterance && utterance
|
||||
&& validatedCommandContext.utterance !== utterance
|
||||
&& !validatedCommandContext.utterance.includes(utterance)
|
||||
@@ -83,6 +105,123 @@
|
||||
: 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 || {};
|
||||
@@ -397,15 +536,68 @@
|
||||
};
|
||||
};
|
||||
const commands = [];
|
||||
configurationEntries.forEach(([_configKey, configuration]) => {
|
||||
(configuration?.spec?.operations || []).forEach((operation) => {
|
||||
const item = parsedCanCommandForOperation(operation);
|
||||
if (item) commands.push({ ...item, ...deliveryForOperation(operation) });
|
||||
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;
|
||||
commands.push({
|
||||
...fallback,
|
||||
channel,
|
||||
command,
|
||||
...(frames.length ? {
|
||||
frame: frames[0].value,
|
||||
frameLabel: frames[0].label,
|
||||
frames,
|
||||
} : {}),
|
||||
semantic: semanticControlInstruction(operation),
|
||||
operationRef: String(operation.id || ""),
|
||||
...deliveryForOperation(operation),
|
||||
});
|
||||
});
|
||||
});
|
||||
} 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.channel}:${item.command}`;
|
||||
const key = `${item.operationRef || item.semantic}:${item.channel}:${item.command}`;
|
||||
const previous = unique.get(key);
|
||||
if (!previous
|
||||
|| (previous.deliveryState !== "submitted" && item.deliveryState === "submitted")
|
||||
@@ -425,7 +617,192 @@
|
||||
}[source] || "云端模型";
|
||||
}
|
||||
|
||||
function renderParsedCanCommands(result, { message = "" } = {}) {
|
||||
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");
|
||||
@@ -442,12 +819,31 @@
|
||||
<b class="v2-can-command-delivery" data-delivery-state="${escapeHtml(item.deliveryState)}" title="${escapeHtml(`${item.deliveryTransport} · ${item.deliveryLabel}`)}">${escapeHtml(item.deliveryLabel)}</b>
|
||||
<i class="v2-can-command-category">${escapeHtml(item.category)}</i>
|
||||
</span>
|
||||
<code>${escapeHtml(item.command)}</code>
|
||||
${item.frame ? `<small class="v2-can-command-frame">${escapeHtml(item.frameLabel || "固件原生 CAN0")} · ${escapeHtml(item.frame)}</small>` : ""}
|
||||
<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);
|
||||
}
|
||||
|
||||
@@ -767,6 +1163,11 @@
|
||||
logKeys: new Set(),
|
||||
extensionFingerprint: "",
|
||||
};
|
||||
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;
|
||||
|
||||
@@ -1625,26 +2026,28 @@
|
||||
if (revision !== displaySync.revision) {
|
||||
displaySync.revision = revision;
|
||||
const task = snapshot?.task || {};
|
||||
setTaskPresentation(task);
|
||||
const sourceLabel = task.source === "web_console" ? "网页端" : "车机端";
|
||||
if (task.utterance) {
|
||||
const submittedUnverified = task.result?.execution_outcome === "submitted_unverified"
|
||||
|| task.result?.execution_report?.spec?.verification_status === "submitted_unverified";
|
||||
appendSyncLog({
|
||||
key: `task:${revision}:${task.status}`,
|
||||
category: "task",
|
||||
title: `${sourceLabel}${task.status === "running" ? "下发编排任务" : "任务状态已同步"}`,
|
||||
detail: `${task.utterance} · ${submittedUnverified ? "指令已提交,等待车辆状态确认" : task.status || "idle"}`,
|
||||
status: submittedUnverified ? "info" : ["succeeded", "partially_succeeded"].includes(task.status) ? "success" : task.status === "failed" ? "error" : "info",
|
||||
});
|
||||
}
|
||||
const result = task.result;
|
||||
const requestId = result?.request_id || null;
|
||||
if (result && requestId !== displaySync.requestId) {
|
||||
displaySync.requestId = requestId;
|
||||
window.CognitiveAgentConsole?.applySyncedResult?.(result, {
|
||||
utterance: task.utterance || "",
|
||||
});
|
||||
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
|
||||
@@ -1763,12 +2166,13 @@
|
||||
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>
|
||||
<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>
|
||||
<p>等待准备下发的控制指令</p>
|
||||
</div>`);
|
||||
canCommandPanel.id = "v2CanCommandPanel";
|
||||
syncOverview.append(canCommandPanel);
|
||||
@@ -1875,17 +2279,57 @@
|
||||
if (event.target.matches("select[data-device-control]")) storeDeviceControlDraft(event.target);
|
||||
});
|
||||
$("#v2LogFilter")?.addEventListener("change", renderSyncLogs);
|
||||
$("#requestForm")?.addEventListener("submit", () => {
|
||||
const beginPendingControlRequest = (request = {}) => {
|
||||
beginCommandTiming(request);
|
||||
resetValidatedCommandContext();
|
||||
renderParsedCanCommands(null, { message: "正在解析自然语言并生成控制指令…" });
|
||||
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) => {
|
||||
@@ -1893,11 +2337,15 @@
|
||||
const submittedUnverified = result.execution_outcome === "submitted_unverified"
|
||||
|| result.execution_report?.spec?.verification_status === "submitted_unverified";
|
||||
const commands = parsedCanCommands(result);
|
||||
if (commands.length) {
|
||||
const matchesTimedRequest = commandResultMatchesTimedRequest(result);
|
||||
if (matchesTimedRequest && commands.length) {
|
||||
validatedCommandsVisible = true;
|
||||
rememberValidatedCommandContext(result);
|
||||
renderParsedCanCommands(result);
|
||||
} else if (!validatedCommandsBelongToTask({
|
||||
renderParsedCanCommands(result, {
|
||||
recordTiming: commandTimingState.displayedAt == null,
|
||||
reveal: commandTimingState.requestStartedAt != null,
|
||||
});
|
||||
} else if (matchesTimedRequest && !validatedCommandsBelongToTask({
|
||||
utterance: result.utterance,
|
||||
result,
|
||||
})) {
|
||||
@@ -1917,7 +2365,15 @@
|
||||
});
|
||||
});
|
||||
window.addEventListener("cognitive-agent:request-complete", (event) => {
|
||||
if (event.detail?.error && !validatedCommandsVisible) {
|
||||
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) {
|
||||
|
||||
@@ -10,23 +10,23 @@
|
||||
<link rel="icon" href="/icons/app-icon-192.png" />
|
||||
<title>511工程-智能座舱</title>
|
||||
<link rel="stylesheet" href="/styles.css?v=dual-seat-23" />
|
||||
<link rel="stylesheet" href="/console-v2.css?v=delivery-state-66" />
|
||||
<link rel="stylesheet" href="/console-v2.css?v=delivery-state-68" />
|
||||
<link rel="stylesheet" href="/self-extension.css?v=gscu-tcp-43" />
|
||||
<link rel="stylesheet" href="/speech-input.css?v=sensevoice-7" />
|
||||
<link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-28" />
|
||||
<link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-29" />
|
||||
<link rel="stylesheet" href="/energy-monitor.css?v=energy-monitor-4" />
|
||||
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
|
||||
<script src="/vendor/three/0.147.0/three.min.js" defer></script>
|
||||
<script src="/vendor/three/0.147.0/fflate.min.js" defer></script>
|
||||
<script src="/vendor/three/0.147.0/FBXLoader.js" defer></script>
|
||||
<script src="/network.js?v=android-pad-2" defer></script>
|
||||
<script src="/app.js?v=dual-seat-132" defer></script>
|
||||
<script src="/app.js?v=dual-seat-133" defer></script>
|
||||
<script src="/speech-input.js?v=sensevoice-13" defer></script>
|
||||
<script type="module" src="/gesture-control.js?v=android-pad-9"></script>
|
||||
<script src="/pwa.js?v=android-pad-3" defer></script>
|
||||
<script src="/console-v2.js?v=delivery-state-62" defer></script>
|
||||
<script src="/console-v2.js?v=local-first-66" defer></script>
|
||||
<script src="/self-extension.js?v=gscu-tcp-43" defer></script>
|
||||
<script src="/assistant-chat.js?v=codex-assistant-28" defer></script>
|
||||
<script src="/assistant-chat.js?v=codex-assistant-29" defer></script>
|
||||
<script src="/energy-monitor.js?v=energy-monitor-4" defer></script>
|
||||
</head>
|
||||
<body class="console-v2">
|
||||
@@ -131,8 +131,6 @@
|
||||
<i data-lucide="radio-tower"></i>真实下发
|
||||
</button>
|
||||
</div>
|
||||
<p class="provider-note" id="hardwareNote">正在检查 CAN 适配器与 IAR 固件配置</p>
|
||||
<p class="provider-note" id="hardwareAliasNote">所有座椅能力仅控制副驾实体电机、加热与通风执行器</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group" id="vehicleTransportField">
|
||||
@@ -145,7 +143,6 @@
|
||||
<i data-lucide="wifi"></i>无线 CAN
|
||||
</button>
|
||||
</div>
|
||||
<p class="provider-note" id="vehicleTransportNote">正在检查车辆连接</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group simulation-fault-field" id="simulationFaultField" hidden>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const CACHE_VERSION = "cognitive-agent-pad-v279";
|
||||
const CACHE_VERSION = "cognitive-agent-pad-v283";
|
||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
||||
const APP_SHELL = [
|
||||
@@ -6,18 +6,18 @@ const APP_SHELL = [
|
||||
"/index.html",
|
||||
"/styles.css?v=dual-seat-23",
|
||||
"/network.js?v=android-pad-2",
|
||||
"/app.js?v=dual-seat-132",
|
||||
"/app.js?v=dual-seat-133",
|
||||
"/gesture-control.js?v=android-pad-9",
|
||||
"/pwa.js?v=android-pad-3",
|
||||
"/console-v2.css?v=delivery-state-66",
|
||||
"/console-v2.js?v=delivery-state-62",
|
||||
"/console-v2.css?v=delivery-state-68",
|
||||
"/console-v2.js?v=local-first-66",
|
||||
"/self-extension.css?v=gscu-tcp-43",
|
||||
"/self-extension.js?v=gscu-tcp-43",
|
||||
"/speech-input.css?v=sensevoice-7",
|
||||
"/speech-input.js?v=sensevoice-13",
|
||||
"/speech-audio-worklet.js?v=sensevoice-5",
|
||||
"/assistant-chat.css?v=codex-assistant-28",
|
||||
"/assistant-chat.js?v=codex-assistant-28",
|
||||
"/assistant-chat.css?v=codex-assistant-29",
|
||||
"/assistant-chat.js?v=codex-assistant-29",
|
||||
"/energy-monitor.css?v=energy-monitor-4",
|
||||
"/energy-monitor.js?v=energy-monitor-4",
|
||||
"/assets/pets/self-extension-cat.png?v=3",
|
||||
|
||||
Reference in New Issue
Block a user