优化无线/有线CAN,以及控制指令解析

This commit is contained in:
minghanchen
2026-08-26 20:11:09 +08:00
parent 9ac8fdface
commit 4a0570706f
49 changed files with 8409 additions and 1289 deletions
+117 -25
View File
@@ -22,6 +22,31 @@
let reasoningPickerControl = null;
let validatedCommandsVisible = false;
let validatedCommandContext = { requestId: "", utterance: "" };
function resetValidatedCommandContext() {
validatedCommandsVisible = false;
validatedCommandContext = { requestId: "", utterance: "" };
}
function rememberValidatedCommandContext(result = {}) {
validatedCommandContext = {
requestId: String(result.request_id || ""),
utterance: String(result.utterance || "").trim(),
};
}
function validatedCommandsBelongToTask(task = {}) {
if (!validatedCommandsVisible) return false;
const result = task.result && typeof task.result === "object" ? task.result : {};
const requestId = String(result.request_id || "");
const utterance = String(task.utterance || result.utterance || "").trim();
if (validatedCommandContext.requestId && requestId
&& validatedCommandContext.requestId !== requestId) return false;
if (validatedCommandContext.utterance && utterance
&& validatedCommandContext.utterance !== utterance) return false;
return true;
}
function make(tag, className, html) {
const node = document.createElement(tag);
@@ -71,10 +96,13 @@
...(parseSource ? { parseSource } : {}),
});
const hexByte = (value) => Number(value).toString(16).padStart(2, "0").toUpperCase();
const otaCan0Frame = (opcode, selector, argument) => {
const numeric = Number(argument) >>> 0;
const argumentBytes = [0, 8, 16, 24].map((shift) => hexByte((numeric >>> shift) & 0xFF));
return `0x6A0 · C7 <SEQ> ${hexByte(opcode)} ${hexByte(selector)} ${argumentBytes.join(" ")}`;
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)}`;
};
const isPassengerSeat = new Set(["passenger", "front-right"]).has(target);
@@ -88,7 +116,6 @@
const axis = passengerAxes[capability];
const position = boundedInteger(parameters.position_pct, 0, 100);
if (axis && position !== null) {
const targetPermille = position * 10;
const motion = operation.motion || {};
const delta = boundedInteger(motion.delta_position_pct, 1, 100);
const relativeDirections = {
@@ -104,7 +131,9 @@
? `seat passenger ${axis[1]} relative ${relativeDirection} ${delta}`
: `seat passenger ${axis[1]} position ${position}`,
"CAN0",
otaCan0Frame(0x64, axis[2], targetPermille),
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)) {
@@ -124,14 +153,12 @@
if (capability === "seat.start_dynamic_mode") {
const mode = boundedInteger(parameters.mode_id, 1, 2);
if (mode !== null) {
const dynamic = mode === 1
? { name: "rocking-horse", selector: 2 }
: { name: "cradle", selector: 1 };
const dynamic = mode === 1 ? "rocking-horse" : "cradle";
return command(
"副驾座椅动态模式",
`seat passenger dynamic ${dynamic.name}`,
`seat passenger dynamic ${dynamic}`,
"CAN0",
otaCan0Frame(0x66, dynamic.selector, 300),
nativeCan0Frame(0x0E, 0, 0, mode, 0),
);
}
}
@@ -140,7 +167,7 @@
"副驾座椅动态模式",
"seat passenger dynamic stop",
"CAN0",
otaCan0Frame(0x63, 0xFF, 0),
nativeCan0Frame(0x0F, 0, 0, 0, 0),
);
}
}
@@ -198,6 +225,13 @@
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);
@@ -330,7 +364,7 @@
${escapeHtml(item.category)}
</span>
<code>${escapeHtml(item.command)}</code>
${item.frame ? `<small class="v2-can-command-frame">seat-ota · ${escapeHtml(item.frame)}</small>` : ""}
${item.frame ? `<small class="v2-can-command-frame">固件原生 CAN0 · ${escapeHtml(item.frame)}</small>` : ""}
</li>`).join("")}</ol>`;
} else {
list.innerHTML = `<p>${escapeHtml(message || "本次请求未生成已验证的 CAN 控制指令")}</p>`;
@@ -359,6 +393,10 @@
<code>${escapeHtml(item.command)}</code>
</li>`).join("")}</ol>`;
validatedCommandsVisible = true;
validatedCommandContext = {
requestId: "",
utterance: items.map((item) => item.command).join(";"),
};
requestAnimationFrame(syncSidebarLogBaseline);
}
@@ -1184,6 +1222,12 @@
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"],
@@ -1192,7 +1236,9 @@
needs_confirmation: ["等待确认", "partial"],
failed: ["执行失败", "failed"],
};
const [label, state] = statusMap[normalizedStatus] || statusMap.idle;
const [label, state] = submittedUnverified && normalizedStatus === "succeeded"
? ["指令已提交 · 等待状态确认", "partial"]
: statusMap[normalizedStatus] || statusMap.idle;
const badge = $("#v2TaskState");
if (badge) {
badge.textContent = label;
@@ -1207,9 +1253,23 @@
});
renderTaskTimeline(task);
if (normalizedStatus === "running") {
renderParsedCanCommands(null, { message: "正在解析自然语言并生成 CAN 控制指令…" });
// 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: "正在解析自然语言并生成 CAN 控制指令…" });
}
} else if (result) {
renderParsedCanCommands(result);
const commands = parsedCanCommands(result);
if (commands.length) {
validatedCommandsVisible = true;
rememberValidatedCommandContext(result);
renderParsedCanCommands(result);
} else if (!validatedCommandsBelongToTask(task)) {
resetValidatedCommandContext();
renderParsedCanCommands(result);
}
}
}
@@ -1245,12 +1305,14 @@
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} · ${task.status || "idle"}`,
status: ["succeeded", "partially_succeeded"].includes(task.status) ? "success" : task.status === "failed" ? "error" : "info",
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;
@@ -1487,26 +1549,44 @@
});
$("#v2LogFilter")?.addEventListener("change", renderSyncLogs);
$("#requestForm")?.addEventListener("submit", () => {
validatedCommandsVisible = false;
resetValidatedCommandContext();
renderParsedCanCommands(null, { message: "正在解析自然语言并生成 CAN 控制指令…" });
});
window.addEventListener("cognitive-agent:validated-can-commands", (event) => {
const result = event.detail || {};
validatedCommandsVisible = parsedCanCommands(result).length > 0;
if (validatedCommandsVisible) rememberValidatedCommandContext(result);
else resetValidatedCommandContext();
renderParsedCanCommands(result, {
message: "已完成白名单校验,正在下发控制指令…",
});
});
window.addEventListener("cognitive-agent:result", (event) => {
const result = event.detail || {};
validatedCommandsVisible = parsedCanCommands(result).length > 0;
renderParsedCanCommands(result);
const submittedUnverified = result.execution_outcome === "submitted_unverified"
|| result.execution_report?.spec?.verification_status === "submitted_unverified";
const commands = parsedCanCommands(result);
if (commands.length) {
validatedCommandsVisible = true;
rememberValidatedCommandContext(result);
renderParsedCanCommands(result);
} else if (!validatedCommandsBelongToTask({
utterance: result.utterance,
result,
})) {
resetValidatedCommandContext();
renderParsedCanCommands(result);
}
appendSyncLog({
key: result.request_id ? `result:${result.request_id}` : "",
category: "task",
title: ["succeeded", "partially_succeeded"].includes(result.status) ? "编排任务执行完成" : "编排任务返回结果",
detail: `${result.summary || result.message || "执行反馈已同步"} · ${result.status || "unknown"}`,
status: ["succeeded", "partially_succeeded"].includes(result.status) ? "success" : result.status === "failed" ? "error" : "info",
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) => {
@@ -1514,7 +1594,19 @@
renderParsedCanCommands(null, { message: "本次请求未生成 CAN 控制指令" });
}
if (event.detail?.error) {
appendSyncLog({ category: "task", title: "编排任务执行失败", detail: event.detail.error.message || "请求失败", status: "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) => {