feat: make mode creation preview-only and cache-safe

This commit is contained in:
Miayui
2026-09-20 19:32:03 +08:00
parent bfff1b515c
commit 89707a42e2
28 changed files with 1375 additions and 165 deletions
+167 -14
View File
@@ -569,6 +569,11 @@
"seat.set_backrest_recline": "靠背",
"seat.set_legrest_height": "腿托",
"seat.set_legrest_extension": "腿托",
"seat.set_ventilation": "通风",
"seat.set_heating": "加热",
"seat.set_driver_ventilation_native": "通风",
"seat.set_driver_heating_native": "加热",
"seat.set_driver_massage_native": "按摩",
}[capability];
if (component) return `${seat}${component}`;
if (capability === "seat.set_driver_axis_native") {
@@ -585,7 +590,19 @@
return seat;
}
if (capability.startsWith("outlet.")) return "空调风口";
return String(fallback || capability || "控制指令");
const domain = String(operation.domain || capability.split(".", 1)[0] || "");
return {
climate: "空调",
window: "车窗",
roof: "遮阳帘",
ambient_light: "氛围灯",
interior_light: "室内灯",
self_extension: "红外外设",
door: "车门",
outlet: "空调风口",
}[domain] || (/^[\u4e00-\u9fff]/.test(String(fallback || ""))
? String(fallback)
: "控制指令");
}
function fallbackAssistantControlCommands(result = {}) {
@@ -620,6 +637,8 @@
}
function assistantDispatchedCommands(result = {}) {
const previewOnly = String(result.execution_mode || "dry_run") !== "hardware"
|| result.orchestration_route === "mode_profile_create_preview";
const projections = Array.isArray(result.validated_control_commands)
? result.validated_control_commands
: [];
@@ -652,22 +671,30 @@
} catch (_error) {
commands = [];
}
if (!commands.length) return fallbackAssistantControlCommands(result);
if (!commands.length) commands = fallbackAssistantControlCommands(result);
return commands.map((item, index) => {
const operation = projectionByRef.get(String(item.operationRef || ""))
|| operationByRef.get(String(item.operationRef || ""))
|| projections[index]?.operation
|| {};
const pendingDelivery = result.status === "processing"
const pendingDelivery = result.status === "processing" && !previewOnly
? {
deliveryState: "submitting",
deliveryLabel: "正在下发",
deliveryDetail: "",
}
: {};
const previewDelivery = previewOnly
? {
deliveryState: "not_submitted",
deliveryLabel: "仅预览 · 未下发",
deliveryDetail: "创建模式期间不会发送控制指令",
}
: {};
return {
...item,
...pendingDelivery,
...previewDelivery,
category: assistantControlCategory(operation, item.category),
operation,
};
@@ -691,6 +718,11 @@
manual: "手动",
internal: "内循环",
external: "外循环",
romantic: "浪漫",
cinema: "影院",
rest: "休息",
camping: "露营",
meditation: "冥想",
}[normalized] || String(value ?? "").trim();
}
@@ -764,6 +796,9 @@
message.dispatchedControlResult = mergedResult;
const commands = assistantDispatchedCommands(mergedResult);
if (!commands.length) return;
const previewOnly = String(mergedResult.execution_mode || "dry_run") !== "hardware"
|| mergedResult.orchestration_route === "mode_profile_create_preview";
const headingText = previewOnly ? "控制指令预览(未下发)" : "下发控制指令";
const signature = JSON.stringify(commands.map((item) => [
item.operationRef,
@@ -794,7 +829,7 @@
deliveryLabel: String(item.deliveryLabel || "未下发"),
deliveryDetail: String(item.deliveryDetail || ""),
}));
if (vueRenderer(host, viewCommands)) {
if (vueRenderer(host, viewCommands, { heading: headingText })) {
host.dataset.commandSignature = signature;
message.article?.classList.add("assistant-message-with-dispatched-controls");
focusLatestDispatchedControls(host);
@@ -821,7 +856,7 @@
headingIcon.className = "assistant-dispatched-controls-heading-icon";
headingIcon.innerHTML = '<i data-lucide="list-tree"></i>';
const heading = document.createElement("b");
heading.textContent = "下发控制指令";
heading.textContent = headingText;
const count = document.createElement("strong");
count.className = "assistant-dispatched-controls-count";
const chevron = document.createElement("i");
@@ -835,6 +870,8 @@
}
card.dataset.commandSignature = signature;
const heading = $("summary b", card);
if (heading) heading.textContent = headingText;
const count = $(".assistant-dispatched-controls-count", card);
const list = $(".assistant-dispatched-controls-list", card);
if (count) count.textContent = String(commands.length);
@@ -925,6 +962,30 @@
indexes.forEach((index) => consumed.add(index));
rows.push({ label, value, icon, order });
};
const domainLabels = {
climate: "空调",
window: "车窗",
roof: "遮阳帘",
seat: "座椅",
ambient_light: "氛围灯",
interior_light: "室内灯",
self_extension: "红外外设",
door: "车门",
outlet: "空调风口",
};
const capabilityLabels = {
"climate.set_power": "空调电源",
"climate.set_temperature": "空调温度",
"climate.set_fan_level": "空调风量",
"climate.set_cycle_mode": "空调循环",
"climate.set_control_mode": "空调控制模式",
"climate.set_outlet_mode": "空调出风模式",
"window.set_position": "车窗位置",
"roof.set_shade_position": "遮阳帘位置",
"ambient_light.set_power": "氛围灯",
"ambient_light.set_mode": "氛围灯模式",
"infrared.emit": "红外外设",
};
const zeroGravityIndexes = actions
.map((action, index) => ({ action, index }))
@@ -978,6 +1039,65 @@
if (position === 100) return `完全打开(${target})`;
return target;
};
const localizedParameterValue = (value) => ({
on: "开启", off: "关闭", open: "打开", close: "关闭", stop: "停止",
auto: "自动", manual: "手动", internal: "内循环", external: "外循环",
romantic: "浪漫", cinema: "影院", rest: "休息", camping: "露营",
}[String(value)] || String(value));
const genericParameterText = (params) => Object.entries(params)
.filter(([key]) => key !== "slot" && key !== "capability_ref")
.map(([key, rawValue]) => {
const name = {
state: "状态", motion: "动作", action: "动作", mode: "模式",
level: "档位", temperature_c: "温度", position_pct: "目标开度",
brightness_pct: "亮度", brightness_level: "亮度档位",
color_hex: "颜色", color_index: "色号",
}[key] || "参数";
const suffix = key === "temperature_c" ? "℃"
: ["position_pct", "brightness_pct"].includes(key) ? "%"
: key === "level" || key === "brightness_level" ? "档" : "";
return `${name}:${localizedParameterValue(rawValue)}${suffix}`;
})
.join(" · ");
const passengerComfort = actions
.map((action, index) => ({ action, index }))
.filter(({ action, index }) => !consumed.has(index)
&& ["passenger", "front_right"].includes(targetZone(action))
&& [
"seat.set_ventilation",
"seat.set_backrest_recline",
"seat.set_legrest_height",
].includes(String(action.capability || "")));
const comfortByCapability = new Map(passengerComfort.map(({ action, index }) => [
String(action.capability || ""), { action, index },
]));
if ([
"seat.set_ventilation",
"seat.set_backrest_recline",
"seat.set_legrest_height",
].every((capability) => comfortByCapability.has(capability))) {
const ventilation = parameters(comfortByCapability.get("seat.set_ventilation").action);
const backrest = parameters(comfortByCapability.get("seat.set_backrest_recline").action);
const legrest = parameters(comfortByCapability.get("seat.set_legrest_height").action);
const comfortMentions = ["座椅", "副驾", "舒适"]
.map((term) => instruction.indexOf(term))
.filter((index) => index >= 0);
const comfortOrder = comfortMentions.length
? Math.min(...comfortMentions)
: 10_000 + Math.max(...passengerComfort.map(({ index }) => index));
addRow(
"副驾座椅舒适模式",
[
`通风${ventilation.state === "off" ? "关闭" : `开启 ${ventilation.level ?? 1}档`}`,
`靠背 ${backrest.position_pct ?? "--"}%`,
`腿托 ${legrest.position_pct ?? "--"}%`,
].join(" · "),
"armchair",
comfortOrder,
passengerComfort.map(({ index }) => index),
);
}
actions.forEach((action, index) => {
if (consumed.has(index)) return;
const capability = String(action.capability || "");
@@ -988,12 +1108,20 @@
front_row: "前排", rear_row: "后排", rear_left: "左后", rear_right: "右后",
}[zone] || "";
const params = parameters(action);
let label = capability || domain || "座舱设备";
let label = capabilityLabels[capability] || domainLabels[domain] || "座舱设备";
let value = "已配置";
let icon = "sliders-horizontal";
let order = 10_000 + index;
if (capability === "roof.set_shade_motion" || capability === "roof.set_shade_position") {
if (capability === "infrared.emit" || domain === "self_extension") {
label = String(action?.target?.name || action?.target?.device || "红外外设").trim()
|| "红外外设";
value = params.action != null
? `动作:${localizedParameterValue(params.action)}`
: "动作已配置";
icon = "radio-tower";
order = instructionOrder(label, "外设", "灯");
} else if (capability === "roof.set_shade_motion" || capability === "roof.set_shade_position") {
label = "遮阳帘";
value = params.position_pct != null
? shadePositionLabel(params.position_pct)
@@ -1011,7 +1139,13 @@
rear_right: "右后车窗",
};
label = windowLabels[zone] || "车窗";
value = params.position_pct != null ? `${params.position_pct}%` : motionLabel(params.motion);
value = params.position_pct != null
? Number(params.position_pct) === 0
? "完全关闭(目标开度 0%)"
: Number(params.position_pct) === 100
? "完全打开(目标开度 100%)"
: `目标开度 ${params.position_pct}%`
: motionLabel(params.motion);
icon = "panels-top-left";
order = instructionOrder("车窗");
} else if (domain === "seat") {
@@ -1071,7 +1205,8 @@
face_feet_defrost: "吹面 / 吹脚 / 除霜", face_defrost: "吹面 / 除霜",
},
};
label = capability === "climate.set_fan_level" ? "空调风量"
label = capability === "climate.set_power" ? "空调电源"
: capability === "climate.set_fan_level" ? "空调风量"
: capability === "climate.set_cycle_mode" ? "空调循环"
: capability === "climate.set_control_mode" ? "空调控制模式"
: capability === "climate.set_outlet_mode" ? "空调出风模式" : "空调";
@@ -1104,6 +1239,9 @@
icon = "lightbulb";
order = instructionOrder(label, "灯光");
}
if (value === "已配置") {
value = genericParameterText(params) || "参数已配置";
}
addRow(label, value, icon, order, [index]);
});
return rows.sort((left, right) => left.order - right.order);
@@ -1219,12 +1357,12 @@
card.classList.add("is-busy");
status.hidden = false;
status.className = "assistant-mode-draft-status working";
status.textContent = "正在进行模拟试运行…";
status.textContent = "正在重新进行 dry-run 校验(不会下发)…";
try {
const result = await window.CognitiveAgentConsole?.tryModeDraft?.({ ...draft, actions });
if (!result || result.status !== "succeeded") throw new Error("试运行未通过,请检查动作设置。");
status.className = "assistant-mode-draft-status success";
status.textContent = "试运行通过 · 尚未保存";
status.textContent = "校验通过 · 未下发 · 尚未保存";
} catch (error) {
status.className = "assistant-mode-draft-status failed";
status.textContent = error.message || "试运行失败,请重试。";
@@ -1275,7 +1413,7 @@
const eyebrow = document.createElement("small");
eyebrow.textContent = draft.existing_mode_id
? "已载入现有功能模式"
: "已为你生成功能模式";
: "已为你生成功能模式(未下发)";
const title = document.createElement("strong");
title.textContent = `「${draft.name}」`;
headingCopy.append(eyebrow, title);
@@ -1314,6 +1452,10 @@
} else {
appendModeDraftRows(list, modeDraftRows(draft, actions));
}
const previewNotice = document.createElement("p");
previewNotice.className = "assistant-mode-draft-preview-notice";
previewNotice.textContent = "创建阶段仅生成并校验控制方案,不会向 CAN 或红外外设发送指令。";
list.append(previewNotice);
const controls = document.createElement("div");
controls.className = "assistant-mode-draft-actions";
@@ -1326,7 +1468,7 @@
const trial = document.createElement("button");
trial.type = "button";
trial.className = "assistant-mode-draft-trial";
trial.textContent = "先试运行";
trial.textContent = "重新校验(不下发)";
controls.append(save, trial);
const status = document.createElement("p");
status.className = "assistant-mode-draft-status";
@@ -1379,12 +1521,23 @@
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
}
function isModeCreationPreviewRequest(message) {
const compact = String(message || "").trim().replace(/\s+/g, "");
if (!compact || /取消(?:创建|构建|生成)|(?:不要|别|禁止)(?:创建|构建|生成)/.test(compact)) {
return false;
}
return /(?:构建|创建|新建|新增|生成|添加)(?:一下)?(?:一个|个|一套|套)?[^,。!?!?]{1,64}?模式/.test(compact);
}
function requestBody(message) {
const executionMode = window.CognitiveAgentConsole?.getExecutionMode?.() || "dry_run";
const configuredExecutionMode = window.CognitiveAgentConsole?.getExecutionMode?.() || "dry_run";
const controlTransport = localStorage.getItem("cognitive-agent.control-transport") || "wired_can";
const hardwareClientId = localStorage.getItem("cognitive-agent.hardware-client-id");
const vehicleId = $("#vehicleSelect")?.value || null;
const modeProfileDraftContext = requestModeDraftContext(message);
const modeConfigurationPreview = Boolean(modeProfileDraftContext)
|| isModeCreationPreviewRequest(message);
const executionMode = modeConfigurationPreview ? "dry_run" : configuredExecutionMode;
return {
message,
conversation_id: conversationId(),