feat: add seat-scoped static and dynamic assistant modes

This commit is contained in:
vee
2026-09-12 14:56:30 +08:00
parent d09b5967f8
commit d8dad22ec0
60 changed files with 5847 additions and 149 deletions
+217 -13
View File
@@ -4860,7 +4860,18 @@ function modeOperationParameters(operation) {
return Object.entries(parameters);
}
function formatModeOperationValue(name, value) {
function formatModeOperationValue(name, value, capability = "") {
if (name === "level" && ["seat.set_driver_ventilation_native", "seat.set_driver_heating_native"].includes(capability)) {
return { 1: "高档", 2: "低档" }[value] || formatConfigValue(name, value);
}
if (capability === "seat.set_driver_massage_native") {
const labels = {
action: { mode: "切换按摩模式", strength: "调节按摩力度", off: "关闭" },
mode: { wave: "波浪", pulse: "脉冲", stretch: "舒展", slack: "舒缓", relax: "放松" },
strength: { 1: "轻柔", 2: "中等", 3: "较强" },
};
if (labels[name]?.[value]) return labels[name][value];
}
const valueLabels = {
on: "开启",
off: "关闭",
@@ -4887,7 +4898,7 @@ function modeOperationView(operation) {
const parameters = modeOperationParameters(operation)
.map(([name, value]) => {
const label = CONFIG_PARAMETER_LABELS[name] || name;
return `${label}:${formatModeOperationValue(name, value)}`;
return `${label}:${formatModeOperationValue(name, value, capability)}`;
})
.join(",");
return {
@@ -4926,7 +4937,50 @@ function modeTimelineStepView(step, action) {
};
}
function modeScheduleDurationLabel(durationMs) {
const minutes = Math.floor(durationMs / 60000);
const seconds = Number(((durationMs % 60000) / 1000).toFixed(3));
return minutes ? `${minutes}分${seconds ? `${seconds}秒` : ""}` : `${seconds}秒`;
}
function modeScheduleView(mode) {
const schedule = mode?.schedule;
if (
schedule?.schema !== "cognitive-agent-mode-schedule/v1"
|| !Array.isArray(schedule.phases) || !schedule.phases.length
|| !Number.isInteger(schedule.repeat_count)
|| schedule.repeat_count < 1 || schedule.repeat_count > 20
|| schedule.phases.some((phase) => !Number.isFinite(phase?.duration_ms)
|| phase.duration_ms <= 0 || !Array.isArray(phase.operations))
) return null;
let offsetMs = 0;
const phases = schedule.phases.map((phase, index) => {
const view = {
name: String(phase.name || `阶段 ${index + 1}`),
timing: `+${modeScheduleDurationLabel(offsetMs)} 开始 · 持续 ${modeScheduleDurationLabel(phase.duration_ms)}`,
actions: phase.operations.map(modeOperationView),
};
offsetMs += phase.duration_ms;
return view;
});
return {
phases,
summary: `${phases.length} 个阶段 · 重复 ${schedule.repeat_count} 次 · 总时长 ${modeScheduleDurationLabel(offsetMs * schedule.repeat_count)}`,
stopHint: "执行后可输入“停止动态模式”提前结束,已执行状态不自动回滚;时间为每轮的相对时间。",
};
}
function modeExecutionActionViews(mode) {
const schedule = modeScheduleView(mode);
if (schedule) {
// Runtime operations may contain a flattened projection. Prefer phases so
// repeated device targets remain distinct and are not listed twice.
return schedule.phases.flatMap((phase, index) => phase.actions.map((action) => ({
...action,
phase: `阶段 ${index + 1}:${phase.name}`,
timing: phase.timing,
})));
}
const operations = Array.isArray(mode?.operations)
? mode.operations.map(modeOperationView)
: [];
@@ -4947,6 +5001,57 @@ function modeExecutionActionViews(mode) {
return [...operations, ...timelineSteps];
}
function modeScheduleTrialMatches(draft, payload) {
if (!draft?.schedule) return true;
if (!modeScheduleView(draft) || !modeScheduleView(payload)) return false;
const signature = (schedule) => JSON.stringify([
schedule.schema, schedule.repeat_count,
schedule.phases.map((phase) => [
phase.id, phase.name, phase.duration_ms,
phase.operations.map((operation) => [
operation.domain, operation.capability,
operation.target_scope || operation.target?.instance || operation.target?.zone,
modeOperationParameters(operation).sort(([left], [right]) => left.localeCompare(right)),
]),
]),
]);
const execution = payload.schedule_execution;
const totalDurationMs = draft.schedule.phases.reduce((total, phase) => total + phase.duration_ms, 0)
* draft.schedule.repeat_count;
return signature(draft.schedule) === signature(payload.schedule)
&& execution?.status === "completed" && execution.completed === true
&& execution.virtual_elapsed_ms === totalDurationMs;
}
function modeScheduleExecutionView(result) {
const execution = result?.schedule_execution;
if (!execution || typeof execution !== "object") return null;
const status = String(execution.status || "");
if (status === "running" && result.status === "running") {
return {
state: "processing", badge: "已启动", title: "动态时间轴已启动",
detail: "后续动作将按阶段执行;可输入“停止动态模式”提前结束。已执行的设备状态不会自动回滚。",
};
}
if (["stopped", "cancelled"].includes(status)) {
return {
state: "succeeded", badge: "已停止", title: "动态时间轴已停止",
detail: "后续阶段不再执行;已经执行的设备状态不会自动回滚。",
};
}
if (status === "completed" && execution.completed === true) {
const simulated = execution.virtual_elapsed_ms != null;
return {
state: "succeeded", badge: simulated ? "模拟通过" : "已结束",
title: simulated ? "动态时间轴模拟试运行通过" : "动态时间轴已结束",
detail: simulated
? "已使用虚拟时钟校验全部阶段;未按真实时间等待,也未向车辆下发控制。"
: "已处理全部计划阶段,请以车辆反馈确认各设备的实际状态。",
};
}
return null;
}
function isUserModeProfile(mode) {
return mode?.origin === "user" || mode?.deletable === true;
}
@@ -4954,6 +5059,7 @@ function isUserModeProfile(mode) {
function isDynamicModeProfile(mode) {
const behavior = String(mode?.behavior || "").trim().toLowerCase();
if (["cyclic", "dynamic"].includes(behavior)) return true;
if (modeScheduleView(mode)) return true;
if (behavior === "static") return false;
if (mode?.timeline && typeof mode.timeline === "object") return true;
return LEGACY_DYNAMIC_MODE_IDS.has(String(mode?.id || ""));
@@ -5034,6 +5140,34 @@ function createModeGalleryCard(mode) {
button.append(iconBox, copy);
shell.append(button);
const schedule = modeScheduleView(mode);
if (schedule) {
meta.textContent = schedule.summary;
const details = document.createElement("details");
details.className = "mode-gallery-schedule-details";
const summary = document.createElement("summary");
summary.textContent = "查看分阶段执行动作";
const list = document.createElement("ol");
schedule.phases.forEach((phase) => {
const item = document.createElement("li");
const title = document.createElement("strong");
title.textContent = phase.name;
const timing = document.createElement("small");
timing.textContent = phase.timing;
item.append(title, timing);
phase.actions.forEach((action) => {
const row = document.createElement("p");
row.textContent = `${action.target} · ${action.action}:${action.parameters}`;
item.append(row);
});
list.append(item);
});
const hint = document.createElement("p");
hint.textContent = schedule.stopHint;
details.append(summary, list, hint);
shell.append(details);
}
const editButton = document.createElement("button");
editButton.className = "mode-gallery-edit";
editButton.type = "button";
@@ -5998,12 +6132,19 @@ function ensureModeActionPopover() {
function renderModeActionPopover(mode) {
const popover = ensureModeActionPopover();
const actions = modeExecutionActionViews(mode);
const schedule = modeScheduleView(mode);
$("[data-mode-action-title]", popover).textContent = mode?.name || mode?.id || "功能模式";
$("[data-mode-action-count]", popover).textContent = actions.length
? `${actions.length} 步`
: `${Number(mode?.operation_count || 0)} 项`;
const list = $("[data-mode-action-list]", popover);
list.replaceChildren();
if (schedule) {
const summary = document.createElement("li");
summary.className = "mode-schedule-popover-summary";
summary.textContent = `${schedule.summary}。${schedule.stopHint}`;
list.append(summary);
}
if (!actions.length) {
const empty = document.createElement("li");
empty.className = "mode-chip-action-empty";
@@ -6020,6 +6161,12 @@ function renderModeActionPopover(mode) {
copy.className = "mode-chip-action-copy";
const actionName = document.createElement("strong");
actionName.textContent = action.action;
if (action.phase) {
const phase = document.createElement("small");
phase.className = "mode-schedule-popover-phase";
phase.textContent = `${action.phase} · ${action.timing}`;
copy.append(phase);
}
const metadata = document.createElement("small");
const device = document.createElement("span");
device.innerHTML = "<b>设备</b>";
@@ -6189,8 +6336,17 @@ function populateModeChips() {
button.append(runIcon);
}
const timeline = mode.timeline;
const schedule = modeScheduleView(mode);
let cycleBadge = $(".mode-cycle-badge", button);
if (["infinite", "until_stopped_or_timeout"].includes(timeline?.repeat)) {
if (schedule) {
if (!cycleBadge) {
cycleBadge = document.createElement("small");
cycleBadge.className = "mode-cycle-badge";
button.append(cycleBadge);
}
cycleBadge.textContent = schedule.summary;
button.dataset.modeBehavior = "dynamic";
} else if (["infinite", "until_stopped_or_timeout"].includes(timeline?.repeat)) {
if (!cycleBadge) {
cycleBadge = document.createElement("small");
cycleBadge.className = "mode-cycle-badge";
@@ -6222,7 +6378,7 @@ function populateModeChips() {
: `${phase.name} 目标到位`;
}).join(" → ")
: "";
button.title = [mode.description || mode.name, phaseSummary, "悬浮或聚焦查看全部执行动作"]
button.title = [mode.description || mode.name, schedule?.summary || phaseSummary, "悬浮或聚焦查看全部执行动作"]
.filter(Boolean)
.join("\n");
});
@@ -6336,7 +6492,7 @@ function setModeExecutionState(modeProfileId, modeState = "idle") {
$$(".mode-chips button[data-mode-profile-id], #modeGallery button[data-mode-profile-id]").forEach((button) => {
const active = button.dataset.modeProfileId === modeProfileId && modeState !== "idle";
button.classList.toggle("running", active && modeState === "running");
button.classList.toggle("succeeded", active && modeState === "succeeded");
button.classList.toggle("succeeded", active && ["succeeded", "started", "ended", "stopped"].includes(modeState));
button.classList.toggle("failed", active && modeState === "failed");
button.dataset.modeState = active ? modeState : "idle";
button.setAttribute("aria-pressed", active ? "true" : "false");
@@ -6345,6 +6501,12 @@ function setModeExecutionState(modeProfileId, modeState = "idle") {
if (stateLabel) {
stateLabel.textContent = active && modeState === "running"
? "执行中"
: active && modeState === "started"
? "已启动"
: active && modeState === "ended"
? "已结束"
: active && modeState === "stopped"
? "已停止"
: active && modeState === "succeeded"
? "已完成"
: active && modeState === "failed"
@@ -6352,7 +6514,7 @@ function setModeExecutionState(modeProfileId, modeState = "idle") {
: "点击执行";
}
});
if (modeProfileId && ["succeeded", "failed"].includes(modeState)) {
if (modeProfileId && ["succeeded", "failed", "started", "ended", "stopped"].includes(modeState)) {
state.modeRunTimer = window.setTimeout(() => setModeExecutionState(null), 2800);
}
}
@@ -6407,14 +6569,19 @@ async function executeSavedModeProfile(mode) {
);
if (!payload) throw new Error("模式卡片未返回执行结果");
renderResult(payload);
const scheduleRunning = modeScheduleExecutionView(payload)?.state === "processing";
const completed = ["succeeded", "partially_succeeded"].includes(payload.status);
$("#utterance").value = mode.name || mode.id;
$("#resultTitle").textContent = completed
$("#resultTitle").textContent = scheduleRunning
? `${mode.name}时间轴已启动`
: completed
? `${mode.name}执行完成`
: `${mode.name}执行未完成`;
setModeExecutionState(mode.id, completed ? "succeeded" : "failed");
setModeExecutionState(mode.id, scheduleRunning ? "started" : completed ? "succeeded" : "failed");
if (state.executionMode === "hardware") await synchronizePhysicalSeatState();
showToast(completed
showToast(scheduleRunning
? "时间轴已启动,可输入“停止动态模式”提前结束"
: completed
? `${mode.name}已直接执行保存的控制指令流`
: `${mode.name}未完成,请查看执行结果`);
} catch (error) {
@@ -7489,6 +7656,9 @@ const CONFIG_DOMAIN_META = {
const CONFIG_CAPABILITY_LABELS = {
"seat.set_ventilation": "设置座椅通风",
"seat.set_heating": "设置座椅加热",
"seat.set_driver_ventilation_native": "设置主驾座椅通风",
"seat.set_driver_heating_native": "设置主驾座椅加热",
"seat.set_driver_massage_native": "设置主驾座椅按摩",
"seat.set_longitudinal": "调节座椅前后位置",
"seat.set_longitudinal_position": "调节座椅前后位置",
"seat.set_cushion_tilt": "调节座椅高度",
@@ -7519,6 +7689,8 @@ const CONFIG_CAPABILITY_LABELS = {
"ambient_light.set_illumination": "设置氛围灯颜色与亮度",
"ambient_light.set_color": "设置氛围灯颜色",
"ambient_light.set_brightness": "设置氛围灯亮度",
"ambient_light.set_brightness_level": "设置氛围灯亮度",
"ambient_light.set_color_index": "设置氛围灯色号",
"ambient_light.set_mode": "设置氛围灯模式",
"roof.set_transmittance": "设置天幕透光率",
"roof.set_shade_position": "设置遮阳帘位置",
@@ -7527,6 +7699,10 @@ const CONFIG_CAPABILITY_LABELS = {
};
const CONFIG_PARAMETER_LABELS = {
action: "动作",
strength: "力度",
color_index: "色号",
brightness_level: "亮度档位",
power: "电源",
state: "状态",
level: "档位",
@@ -7813,6 +7989,24 @@ function renderExecution(result) {
const summary = $("#executionSummary");
const list = $("#resultList");
list.replaceChildren();
const scheduleExecution = modeScheduleExecutionView(result);
if (scheduleExecution) {
summary.className = `execution-summary ${scheduleExecution.state === "processing" ? "partial" : scheduleExecution.state}`;
const icon = document.createElement("i");
icon.dataset.lucide = scheduleExecution.state === "processing" ? "timer" : "circle-check";
const copy = document.createElement("div");
const title = document.createElement("strong");
title.textContent = scheduleExecution.title;
const detail = document.createElement("span");
detail.textContent = scheduleExecution.detail;
copy.append(title, detail);
summary.replaceChildren(icon, copy);
$("#resultNavMeta").textContent = scheduleExecution.badge;
$("#resultPanelState").textContent = scheduleExecution.badge;
$("#resultPanelState").className = `workspace-panel-state ${scheduleExecution.state}`;
refreshIcons();
return;
}
const report = result.execution_report;
if (!report) {
summary.className = "execution-summary failed";
@@ -8084,6 +8278,7 @@ window.addEventListener("cognitive-agent:extension-vehicle-result", (event) => {
function renderResult(result) {
state.result = result;
const scheduleExecution = modeScheduleExecutionView(result);
const reportMode = result.execution_report?.spec?.mode;
const feedbackSource = result.execution_report?.metadata?.labels?.seat_feedback;
const submittedUnverified = executionVerificationStatus(result) === "submitted_unverified";
@@ -8118,7 +8313,9 @@ function renderResult(result) {
};
const peripheralRoute = String(result.orchestration_route || "")
.startsWith("nfc_peripheral");
$("#resultTitle").textContent = peripheralRoute && executionSucceeded
$("#resultTitle").textContent = scheduleExecution
? `${result.mode_profile?.name ? `${result.mode_profile.name} · ` : ""}${scheduleExecution.title}`
: peripheralRoute && executionSucceeded
? `${result.peripheral?.name || "第三方外设"} · ${result.peripheral?.action || "动作已执行"}`
: partiallySucceeded
? `${domains.length} 域场景部分完成`
@@ -8142,7 +8339,7 @@ function renderResult(result) {
$("#requestMeta").textContent += ` / reasoning:${result.reasoning_effort_used}`;
}
setRunStatus(
partiallySucceeded || submittedUnverified ? "partial" : executionSucceeded ? "succeeded" : "failed",
scheduleExecution?.state || (partiallySucceeded || submittedUnverified ? "partial" : executionSucceeded ? "succeeded" : "failed"),
submittedUnverified ? "SUBMITTED_UNVERIFIED" : result.status.toUpperCase(),
);
renderDecision(result);
@@ -8722,12 +8919,17 @@ window.CognitiveAgentConsole = {
&& expectedSignatures.every((signature, index) => signature === actualSignatures[index]));
if (payload.status !== "succeeded"
|| (expectedOperationCount > 0 && actualOperationCount < expectedOperationCount)
|| !sameActions) {
|| !sameActions || !modeScheduleTrialMatches(draft, payload)) {
throw new Error("试运行未完整覆盖草稿动作,请重新生成模式预览。");
}
return payload;
},
revealModeProfile,
updateScheduleExecutionStatus(modeProfileId, status) {
if (!modeProfileId || (state.pendingModeProfileId && state.pendingModeProfileId !== modeProfileId)) return;
const terminal = { completed: "ended", failed: "failed", cancelled: "stopped", stopped: "stopped", idle: "idle" }[status];
if (terminal) setModeExecutionState(modeProfileId, terminal);
},
notify(message) {
showToast(String(message));
},
@@ -8750,9 +8952,11 @@ window.addEventListener("cognitive-agent:request-complete", (event) => {
if (!modeProfileId) return;
state.pendingModeProfileId = null;
const result = event.detail?.result;
const scheduleRunning = !event.detail?.error
&& modeScheduleExecutionView(result)?.state === "processing";
const succeeded = !event.detail?.error
&& ["succeeded", "partially_succeeded"].includes(String(result?.status || ""));
setModeExecutionState(modeProfileId, succeeded ? "succeeded" : "failed");
setModeExecutionState(modeProfileId, scheduleRunning ? "started" : succeeded ? "succeeded" : "failed");
});
document.addEventListener("DOMContentLoaded", async () => {