fix: compose named cockpit modes from capabilities

This commit is contained in:
vee
2026-09-07 20:52:19 +08:00
parent cf9b89508c
commit 6b2b644ee6
23 changed files with 2917 additions and 124 deletions
+56 -7
View File
@@ -8310,20 +8310,69 @@ window.CognitiveAgentConsole = {
? draft.trim()
: String(draft?.instruction || "").trim();
if (!instruction) throw new Error("功能模式草稿缺少可试运行的指令。");
const { payload } = await requestNaturalLanguageControl(instruction, {
executionMode: "dry_run",
clientType: "assistant_mode_trial",
provider: "local",
previewOnly: false,
});
let payload;
if (draft && typeof draft === "object" && draft.source_request_id) {
const response = await apiRequest("/api/mode-profile-drafts/trial", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: draft.name,
instruction,
source_request_id: draft.source_request_id,
vehicle_id: draft.vehicle_id || state.vehicleId || null,
}),
timeoutMs: 30 * 1000,
retries: 0,
});
payload = await response.json();
if (response.status === 401) {
window.location.replace("/login");
return null;
}
if (!response.ok) {
throw new Error(payload.message || payload.error || `HTTP ${response.status}`);
}
} else {
({ payload } = await requestNaturalLanguageControl(instruction, {
executionMode: "dry_run",
clientType: "assistant_mode_trial",
provider: "local",
previewOnly: false,
}));
}
if (!payload) throw new Error("模拟试运行没有返回结果。");
const actualOperationCount = Object.values(payload.function_configs || {})
.reduce((total, config) => total + (config?.spec?.operations?.length || 0), 0);
const expectedOperationCount = Number(
typeof draft === "object" ? draft?.operation_count : 0,
);
const actionSignature = (action) => {
const target = action?.target && typeof action.target === "object"
? action.target.instance || action.target.zone || action.target.device || ""
: action?.target || "";
const parameters = action?.parameters && typeof action.parameters === "object"
? Object.fromEntries(Object.entries(action.parameters).sort(([left], [right]) => left.localeCompare(right)))
: {};
return JSON.stringify([
String(action?.domain || ""),
String(action?.capability || ""),
String(target),
parameters,
]);
};
const expectedSignatures = Array.isArray(draft?.actions)
? draft.actions.map(actionSignature).sort()
: [];
const actualActions = Array.isArray(payload?.intent_ir?.spec?.actions)
? payload.intent_ir.spec.actions
: [];
const actualSignatures = actualActions.map(actionSignature).sort();
const sameActions = expectedSignatures.length === 0
|| (expectedSignatures.length === actualSignatures.length
&& expectedSignatures.every((signature, index) => signature === actualSignatures[index]));
if (payload.status !== "succeeded"
|| (expectedOperationCount > 0 && actualOperationCount < expectedOperationCount)) {
|| (expectedOperationCount > 0 && actualOperationCount < expectedOperationCount)
|| !sameActions) {
throw new Error("试运行未完整覆盖草稿动作,请重新生成模式预览。");
}
return payload;
+36 -15
View File
@@ -271,7 +271,7 @@
followLatest();
}
function appendModeCreatedState(message, profile) {
function appendModeCreatedState(message, profile, { updated = false } = {}) {
if (!message?.body || !profile?.id) return;
const selector = `[data-assistant-mode-created="${CSS.escape(String(profile.id))}"]`;
if ($(selector, message.body)) return;
@@ -286,7 +286,7 @@
const copy = document.createElement("span");
copy.className = "assistant-mode-created-copy";
const label = document.createElement("small");
label.textContent = "已添加到「我的模式」";
label.textContent = updated ? "已更新「我的模式」" : "已添加到「我的模式」";
const name = document.createElement("strong");
name.textContent = String(profile.name || "新功能模式");
copy.append(label, name);
@@ -396,9 +396,16 @@
icon = "panels-top-left";
order = instructionOrder("车窗");
} else if (domain === "seat") {
label = ["passenger", "front_right"].includes(zone) ? "副驾座椅" : "主驾座椅";
const seatLabel = ["passenger", "front_right"].includes(zone) ? "副驾" : "主驾";
const seatCapabilityLabels = {
"seat.set_longitudinal_position": "座椅前后位置",
"seat.set_cushion_tilt": "坐垫角度",
"seat.set_backrest_recline": "靠背角度",
"seat.set_legrest_height": "腿托高度",
};
label = `${seatLabel}${seatCapabilityLabels[capability] || "座椅"}`;
value = params.position_pct != null
? `${params.position_pct}%`
? `${params.position_pct}%${params.angle_deg != null ? `(${params.angle_deg}°)` : ""}`
: motionLabel(params.state || params.mode_id);
icon = "armchair";
order = instructionOrder("座椅", "副驾", "主驾");
@@ -408,8 +415,12 @@
icon = "thermometer";
order = instructionOrder("空调", "温度");
} else if (domain === "climate") {
label = "空调";
value = params.level != null ? `${params.level}档` : motionLabel(params.state);
const cycleLabels = { internal: "内循环", external: "外循环", auto: "自动循环" };
label = capability === "climate.set_fan_level" ? "空调风量"
: capability === "climate.set_cycle_mode" ? "空调循环" : "空调";
value = params.level != null ? `${params.level}档`
: params.mode != null ? (cycleLabels[String(params.mode)] || String(params.mode))
: motionLabel(params.state);
icon = "fan";
order = instructionOrder("空调", "风量");
} else if (capability === "interior_light.set_reading_power") {
@@ -433,9 +444,15 @@
card.classList.add("is-busy");
status.hidden = false;
status.className = "assistant-mode-draft-status working";
status.textContent = "正在保存到「我的模式」…";
const existingModeId = String(draft.existing_mode_id || "").trim();
status.textContent = existingModeId
? "正在将验证结果保存为模式更新…"
: "正在保存到「我的模式」…";
try {
const response = await window.CognitiveAgentNetwork.request("/api/mode-profiles", {
const endpoint = existingModeId
? `/api/mode-profiles/${encodeURIComponent(existingModeId)}`
: "/api/mode-profiles";
const response = await window.CognitiveAgentNetwork.request(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
@@ -456,10 +473,10 @@
card.classList.remove("is-busy");
card.classList.add("is-saved");
status.className = "assistant-mode-draft-status success";
status.textContent = "模式已保存";
status.textContent = existingModeId ? "模式已更新" : "模式已保存";
card.querySelector(".assistant-mode-draft-actions")?.setAttribute("hidden", "");
await window.CognitiveAgentConsole?.refreshModeProfiles?.(payload.mode_profile?.id);
appendModeCreatedState(message, payload.mode_profile);
appendModeCreatedState(message, payload.mode_profile, { updated: Boolean(existingModeId) });
persistMessages();
} catch (error) {
card.classList.remove("is-busy");
@@ -469,14 +486,14 @@
}
}
async function tryModeProfileDraft(card, draft, buttons, status) {
async function tryModeProfileDraft(card, draft, actions, buttons, status) {
buttons.forEach((button) => { button.disabled = true; });
card.classList.add("is-busy");
status.hidden = false;
status.className = "assistant-mode-draft-status working";
status.textContent = "正在进行模拟试运行…";
try {
const result = await window.CognitiveAgentConsole?.tryModeDraft?.(draft);
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 = "试运行通过 · 尚未保存";
@@ -501,7 +518,9 @@
const heading = document.createElement("header");
const headingCopy = document.createElement("span");
const eyebrow = document.createElement("small");
eyebrow.textContent = "已为你生成功能模式";
eyebrow.textContent = draft.existing_mode_id
? "已载入现有功能模式"
: "已为你生成功能模式";
const title = document.createElement("strong");
title.textContent = `「${draft.name}」`;
headingCopy.append(eyebrow, title);
@@ -530,7 +549,9 @@
const save = document.createElement("button");
save.type = "button";
save.className = "assistant-mode-draft-save";
save.innerHTML = '<i data-lucide="bookmark-plus"></i><span>保存为功能模式</span>';
save.innerHTML = draft.existing_mode_id
? '<i data-lucide="refresh-cw"></i><span>确认更新此模式</span>'
: '<i data-lucide="bookmark-plus"></i><span>保存为功能模式</span>';
const trial = document.createElement("button");
trial.type = "button";
trial.className = "assistant-mode-draft-trial";
@@ -545,7 +566,7 @@
void saveModeProfileDraft(message, card, draft, buttons, status);
});
trial.addEventListener("click", () => {
void tryModeProfileDraft(card, draft, buttons, status);
void tryModeProfileDraft(card, draft, actions, buttons, status);
});
card.append(heading, list, controls, status);
+2 -2
View File
@@ -20,13 +20,13 @@
<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-126" defer></script>
<script src="/app.js?v=dual-seat-127" defer></script>
<script src="/speech-input.js?v=sensevoice-12" 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="/self-extension.js?v=gscu-tcp-43" defer></script>
<script src="/assistant-chat.js?v=codex-assistant-20" defer></script>
<script src="/assistant-chat.js?v=codex-assistant-22" defer></script>
<script src="/energy-monitor.js?v=energy-monitor-4" defer></script>
</head>
<body class="console-v2">
+3 -3
View File
@@ -1,4 +1,4 @@
const CACHE_VERSION = "cognitive-agent-pad-v269";
const CACHE_VERSION = "cognitive-agent-pad-v271";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [
@@ -6,7 +6,7 @@ const APP_SHELL = [
"/index.html",
"/styles.css?v=dual-seat-23",
"/network.js?v=android-pad-2",
"/app.js?v=dual-seat-126",
"/app.js?v=dual-seat-127",
"/gesture-control.js?v=android-pad-9",
"/pwa.js?v=android-pad-3",
"/console-v2.css?v=delivery-state-65",
@@ -17,7 +17,7 @@ const APP_SHELL = [
"/speech-input.js?v=sensevoice-12",
"/speech-audio-worklet.js?v=sensevoice-5",
"/assistant-chat.css?v=codex-assistant-22",
"/assistant-chat.js?v=codex-assistant-20",
"/assistant-chat.js?v=codex-assistant-22",
"/energy-monitor.css?v=energy-monitor-4",
"/energy-monitor.js?v=energy-monitor-4",
"/assets/pets/self-extension-cat.png?v=3",