feat: add seat-scoped static and dynamic assistant modes
This commit is contained in:
+217
-13
@@ -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 () => {
|
||||
|
||||
@@ -551,6 +551,111 @@
|
||||
padding: 9px 10px 8px;
|
||||
}
|
||||
|
||||
.assistant-mode-schedule-summary,
|
||||
.assistant-mode-schedule-hint {
|
||||
margin: 0;
|
||||
padding: 9px 12px;
|
||||
color: var(--assistant-muted, #64748b);
|
||||
font-size: 10px;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.assistant-mode-schedule-summary {
|
||||
color: var(--status-info-foreground, #3764d9);
|
||||
background: var(--status-info-soft, #eef3ff);
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.assistant-mode-schedule-phase + .assistant-mode-schedule-phase,
|
||||
.assistant-mode-schedule-hint {
|
||||
border-top: 1px solid var(--assistant-border, #e5eaf1);
|
||||
}
|
||||
|
||||
.assistant-mode-schedule-phase > header {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
padding: 10px 12px 5px;
|
||||
color: var(--assistant-heading, #334155);
|
||||
font-size: 11px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.assistant-mode-schedule-phase > header small {
|
||||
color: var(--assistant-muted, #64748b);
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.assistant-mode-draft-scheduled .assistant-mode-draft-row {
|
||||
grid-template-columns: 18px minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.assistant-mode-draft-scheduled .assistant-mode-draft-row > strong {
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.mode-gallery-card-shell:has(> .mode-gallery-schedule-details) > .mode-gallery-card {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.mode-gallery-schedule-details {
|
||||
padding: 9px 12px;
|
||||
color: var(--foreground, #334155);
|
||||
background: var(--card, #fff);
|
||||
border: 1px solid var(--border, #e5eaf1);
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.mode-gallery-schedule-details > summary {
|
||||
cursor: pointer;
|
||||
color: var(--primary, #3764d9);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mode-gallery-schedule-details ol {
|
||||
margin: 10px 0;
|
||||
padding-left: 19px;
|
||||
}
|
||||
|
||||
.mode-gallery-schedule-details li + li {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mode-gallery-schedule-details li > strong,
|
||||
.mode-gallery-schedule-details li > small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mode-gallery-schedule-details small,
|
||||
.mode-gallery-schedule-details > p {
|
||||
color: var(--muted-foreground, #64748b);
|
||||
}
|
||||
|
||||
.mode-gallery-schedule-details p {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.mode-chip-action-popover li.mode-schedule-popover-summary {
|
||||
display: block;
|
||||
color: var(--status-info-foreground, #3764d9);
|
||||
background: var(--status-info-soft, #eef3ff);
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.mode-schedule-popover-phase {
|
||||
color: var(--primary, #3764d9);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.assistant-mode-draft-actions[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
let pendingMessage = null;
|
||||
let latestControlResult = null;
|
||||
let activeModeDraftContext = null;
|
||||
let activeSchedulePoll = null;
|
||||
let initialized = false;
|
||||
|
||||
const $ = (selector, root = document) => root.querySelector(selector);
|
||||
@@ -542,7 +543,7 @@
|
||||
const consumed = new Set();
|
||||
const rows = [];
|
||||
const targetZone = (action) => String(
|
||||
action?.target?.instance || action?.target?.zone || action?.target || "",
|
||||
action?.target?.instance || action?.target?.zone || action?.target_scope || action?.target || "",
|
||||
);
|
||||
const parameters = (action) => (
|
||||
action?.parameters && typeof action.parameters === "object" ? action.parameters : {}
|
||||
@@ -613,6 +614,10 @@
|
||||
const capability = String(action.capability || "");
|
||||
const domain = String(action.domain || "");
|
||||
const zone = targetZone(action);
|
||||
const zoneLabel = {
|
||||
driver: "主驾", passenger: "副驾", front_left: "主驾", front_right: "副驾",
|
||||
front_row: "前排", rear_row: "后排", rear_left: "左后", rear_right: "右后",
|
||||
}[zone] || "";
|
||||
const params = parameters(action);
|
||||
let label = capability || domain || "座舱设备";
|
||||
let value = "已配置";
|
||||
@@ -683,7 +688,7 @@
|
||||
icon = "armchair";
|
||||
order = instructionOrder("座椅", "副驾", "主驾");
|
||||
} else if (capability === "climate.set_temperature") {
|
||||
label = "空调温度";
|
||||
label = `${zoneLabel}空调温度`;
|
||||
value = `${params.temperature_c ?? "--"}℃`;
|
||||
icon = "thermometer";
|
||||
order = instructionOrder("空调", "温度");
|
||||
@@ -701,13 +706,14 @@
|
||||
: capability === "climate.set_cycle_mode" ? "空调循环"
|
||||
: capability === "climate.set_control_mode" ? "空调控制模式"
|
||||
: capability === "climate.set_outlet_mode" ? "空调出风模式" : "空调";
|
||||
label = `${zoneLabel}${label}`;
|
||||
value = params.level != null ? `${params.level}档`
|
||||
: params.mode != null ? (climateModeLabels[capability]?.[String(params.mode)] || String(params.mode))
|
||||
: motionLabel(params.state);
|
||||
icon = "fan";
|
||||
order = instructionOrder("空调", "风量");
|
||||
} else if (capability === "interior_light.set_reading_power") {
|
||||
label = "阅读灯";
|
||||
label = `${zoneLabel}阅读灯`;
|
||||
value = motionLabel(params.state);
|
||||
icon = "book-open";
|
||||
order = instructionOrder("阅读灯", "灯光");
|
||||
@@ -734,6 +740,63 @@
|
||||
return rows.sort((left, right) => left.order - right.order);
|
||||
}
|
||||
|
||||
function modeDraftScheduleView(draft) {
|
||||
const schedule = draft?.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;
|
||||
const durationLabel = (durationMs) => {
|
||||
const minutes = Math.floor(durationMs / 60000);
|
||||
const seconds = Number(((durationMs % 60000) / 1000).toFixed(3));
|
||||
return minutes ? `${minutes}分${seconds ? `${seconds}秒` : ""}` : `${seconds}秒`;
|
||||
};
|
||||
let offsetMs = 0;
|
||||
const phases = schedule.phases.map((phase, index) => {
|
||||
// Each phase is rendered separately, without instruction-based grouping:
|
||||
// the same seat or temperature can have a different target later on.
|
||||
const rows = modeDraftRows({ instruction: "" }, phase.operations.map((operation) => ({
|
||||
...operation,
|
||||
target: operation.target || (operation.domain === "seat"
|
||||
? { instance: operation.target_scope }
|
||||
: { zone: operation.target_scope }),
|
||||
})));
|
||||
const view = {
|
||||
id: String(phase.id || `phase-${index + 1}`),
|
||||
name: String(phase.name || `阶段 ${index + 1}`),
|
||||
timing: `+${durationLabel(offsetMs)} 开始 · 持续 ${durationLabel(phase.duration_ms)}`,
|
||||
rows,
|
||||
};
|
||||
offsetMs += phase.duration_ms;
|
||||
return view;
|
||||
});
|
||||
return {
|
||||
phases,
|
||||
operationCount: schedule.phases.reduce((count, phase) => count + phase.operations.length, 0),
|
||||
summary: `${phases.length} 个阶段 · 重复 ${schedule.repeat_count} 次 · 总时长 ${durationLabel(offsetMs * schedule.repeat_count)}`,
|
||||
stopHint: "执行后可输入“停止动态模式”提前结束,已执行状态不自动回滚。各阶段按顺序执行,时间为每轮的相对时间。",
|
||||
};
|
||||
}
|
||||
|
||||
function appendModeDraftRows(container, rows) {
|
||||
rows.forEach((item) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "assistant-mode-draft-row";
|
||||
const icon = document.createElement("i");
|
||||
icon.dataset.lucide = item.icon;
|
||||
const label = document.createElement("span");
|
||||
label.textContent = item.label;
|
||||
const value = document.createElement("strong");
|
||||
value.textContent = item.value;
|
||||
row.append(icon, label, value);
|
||||
container.append(row);
|
||||
});
|
||||
}
|
||||
|
||||
async function saveModeProfileDraft(message, card, draft, buttons, status) {
|
||||
buttons.forEach((button) => { button.disabled = true; });
|
||||
card.classList.add("is-busy");
|
||||
@@ -849,23 +912,39 @@
|
||||
headingCopy.append(eyebrow, title);
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "assistant-mode-draft-badge";
|
||||
badge.textContent = `${Number(draft.operation_count) || 0} 项动作`;
|
||||
const scheduleView = modeDraftScheduleView(draft);
|
||||
badge.textContent = `${scheduleView?.operationCount ?? (Number(draft.operation_count) || 0)} 项动作`;
|
||||
heading.append(headingCopy, badge);
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.className = "assistant-mode-draft-list";
|
||||
modeDraftRows(draft, actions).forEach((item) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "assistant-mode-draft-row";
|
||||
const icon = document.createElement("i");
|
||||
icon.dataset.lucide = item.icon;
|
||||
const label = document.createElement("span");
|
||||
label.textContent = item.label;
|
||||
const value = document.createElement("strong");
|
||||
value.textContent = item.value;
|
||||
row.append(icon, label, value);
|
||||
list.append(row);
|
||||
});
|
||||
if (scheduleView) {
|
||||
card.classList.add("assistant-mode-draft-scheduled");
|
||||
const summary = document.createElement("p");
|
||||
summary.className = "assistant-mode-schedule-summary";
|
||||
summary.textContent = scheduleView.summary;
|
||||
list.append(summary);
|
||||
scheduleView.phases.forEach((phase, index) => {
|
||||
const section = document.createElement("section");
|
||||
section.className = "assistant-mode-schedule-phase";
|
||||
section.dataset.modeSchedulePhase = phase.id;
|
||||
const phaseHeading = document.createElement("header");
|
||||
const name = document.createElement("strong");
|
||||
name.textContent = `${index + 1}. ${phase.name}`;
|
||||
const timing = document.createElement("small");
|
||||
timing.textContent = phase.timing;
|
||||
phaseHeading.append(name, timing);
|
||||
section.append(phaseHeading);
|
||||
appendModeDraftRows(section, phase.rows);
|
||||
list.append(section);
|
||||
});
|
||||
const hint = document.createElement("p");
|
||||
hint.className = "assistant-mode-schedule-hint";
|
||||
hint.textContent = scheduleView.stopHint;
|
||||
list.append(hint);
|
||||
} else {
|
||||
appendModeDraftRows(list, modeDraftRows(draft, actions));
|
||||
}
|
||||
|
||||
const controls = document.createElement("div");
|
||||
controls.className = "assistant-mode-draft-actions";
|
||||
@@ -965,6 +1044,90 @@
|
||||
}
|
||||
}
|
||||
|
||||
function stopScheduleStatusPoll() {
|
||||
if (!activeSchedulePoll) return;
|
||||
window.clearTimeout(activeSchedulePoll.timer);
|
||||
activeSchedulePoll.controller?.abort();
|
||||
activeSchedulePoll = null;
|
||||
}
|
||||
|
||||
function startScheduleStatusPoll(payload, message) {
|
||||
stopScheduleStatusPoll();
|
||||
const vehicleId = String(payload?.vehicle_id || payload?.vehicle?.id
|
||||
|| payload?.mode_profile?.vehicle_id || $("#vehicleSelect")?.value || "");
|
||||
if (!vehicleId) return;
|
||||
const poll = {
|
||||
vehicleId, conversationId: conversationId(), message,
|
||||
sessionId: String(payload?.schedule_execution?.session_id || ""),
|
||||
runId: String(payload?.schedule_execution?.run_id || ""),
|
||||
modeProfileId: String(payload?.mode_profile?.id || ""),
|
||||
timer: null, controller: null,
|
||||
};
|
||||
activeSchedulePoll = poll;
|
||||
const isCurrent = () => activeSchedulePoll === poll
|
||||
&& conversationId() === poll.conversationId
|
||||
&& (!$("#vehicleSelect")?.value || $("#vehicleSelect").value === poll.vehicleId);
|
||||
const update = async () => {
|
||||
if (!isCurrent()) {
|
||||
if (activeSchedulePoll === poll) stopScheduleStatusPoll();
|
||||
return;
|
||||
}
|
||||
poll.controller = new AbortController();
|
||||
try {
|
||||
const response = await window.CognitiveAgentNetwork.request(
|
||||
`/api/modes/schedule/status?vehicle_id=${encodeURIComponent(poll.vehicleId)}`,
|
||||
{ method: "GET", signal: poll.controller.signal, timeoutMs: 8000, retries: 0 },
|
||||
);
|
||||
if (!isCurrent()) return;
|
||||
if (response.status === 401) {
|
||||
setActionStatus(message, "登录状态已失效,无法继续查询时间轴状态;请重新登录查看。", "failed");
|
||||
stopScheduleStatusPoll();
|
||||
return;
|
||||
}
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const status = await response.json();
|
||||
if (!isCurrent()) return;
|
||||
if (status.vehicle_id && String(status.vehicle_id) !== poll.vehicleId) {
|
||||
throw new Error("时间轴状态车型不匹配");
|
||||
}
|
||||
if ((poll.runId && status.run_id && String(status.run_id) !== poll.runId)
|
||||
|| (poll.sessionId && status.session_id && String(status.session_id) !== poll.sessionId)) {
|
||||
setActionStatus(message, "当前时间轴已被新的运行任务替换,请查看最新模式消息。", "working");
|
||||
stopScheduleStatusPoll();
|
||||
return;
|
||||
}
|
||||
const terminal = ["completed", "failed", "cancelled", "stopped", "idle"].includes(status.status);
|
||||
if (status.status === "running") {
|
||||
const phase = Number.isInteger(status.phase_index) && status.phase_index >= 0
|
||||
? ` · 阶段 ${status.phase_index + 1}` : "";
|
||||
const repeat = Number.isInteger(status.repeat_index) && status.repeat_index >= 0
|
||||
? ` · 第 ${status.repeat_index + 1} 轮` : "";
|
||||
setActionStatus(message, `动态时间轴执行中${repeat}${phase}。可输入“停止动态模式”;已执行状态不自动回滚。`, "working");
|
||||
} else if (terminal) {
|
||||
const stopped = ["cancelled", "stopped"].includes(status.status);
|
||||
const failed = status.status === "failed";
|
||||
const copy = failed
|
||||
? `动态时间轴执行失败:${status.error || "某个阶段未通过执行校验"}。后续阶段不再执行,已执行状态不自动回滚。`
|
||||
: stopped
|
||||
? "动态时间轴已停止,后续阶段不再执行;已执行状态不自动回滚。"
|
||||
: status.status === "completed"
|
||||
? "动态时间轴已结束,全部计划阶段已处理;请以车辆反馈确认设备实际状态。"
|
||||
: "当前没有正在运行的动态时间轴。";
|
||||
setActionStatus(message, copy, failed ? "failed" : "success");
|
||||
window.CognitiveAgentConsole?.updateScheduleExecutionStatus?.(poll.modeProfileId, status.status);
|
||||
stopScheduleStatusPoll();
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isCurrent() || poll.controller?.signal.aborted) return;
|
||||
setActionStatus(message, "暂时无法获取时间轴状态,正在重新查询;需要停止时可输入“停止动态模式”。", "working");
|
||||
} finally {
|
||||
if (isCurrent()) poll.timer = window.setTimeout(update, 2000);
|
||||
}
|
||||
};
|
||||
poll.timer = window.setTimeout(update, 2000);
|
||||
}
|
||||
|
||||
function applyAssistantEvent(event, message) {
|
||||
if (event.type === "answer.delta") {
|
||||
setMessageContent(message, `${message.content}${event.delta || ""}`);
|
||||
@@ -985,14 +1148,20 @@
|
||||
if (event.type === "action.completed" || event.type === "work.completed") {
|
||||
latestControlResult = event.payload || latestControlResult;
|
||||
const completed = event.success !== false;
|
||||
const scheduleRunning = completed
|
||||
&& event.payload?.orchestration_route === "mode_schedule_execute"
|
||||
&& (event.payload?.status === "running" || event.payload?.schedule_execution?.status === "running");
|
||||
const defaultText = event.type === "work.completed"
|
||||
? (completed ? "AI助手已完成工作" : "AI助手未能完成工作")
|
||||
: (completed ? "座舱控制已完成" : "座舱控制未完成");
|
||||
setActionStatus(
|
||||
message,
|
||||
event.display_text || defaultText,
|
||||
completed ? "success" : "failed",
|
||||
scheduleRunning
|
||||
? "已启动动态时间轴;后续动作将分阶段执行。可输入“停止动态模式”提前结束,已执行的设备状态不会自动回滚。"
|
||||
: event.display_text || defaultText,
|
||||
scheduleRunning ? "working" : completed ? "success" : "failed",
|
||||
);
|
||||
if (scheduleRunning) startScheduleStatusPoll(event.payload, message);
|
||||
if (event.payload) {
|
||||
window.CognitiveAgentConsole?.applySyncedResult?.(event.payload);
|
||||
if (
|
||||
@@ -1238,11 +1407,17 @@
|
||||
document.addEventListener("click", (event) => {
|
||||
const target = event.target instanceof Element ? event.target : null;
|
||||
if (!target?.closest("#newConversation")) return;
|
||||
stopScheduleStatusPoll();
|
||||
sessionStorage.removeItem(STORAGE_KEY);
|
||||
clearActiveModeDraftContext();
|
||||
sessionStorage.setItem(CONVERSATION_STORAGE_KEY, createConversationId());
|
||||
}, true);
|
||||
|
||||
document.addEventListener("change", (event) => {
|
||||
if (event.target?.id === "vehicleSelect") stopScheduleStatusPoll();
|
||||
});
|
||||
window.addEventListener("pagehide", stopScheduleStatusPoll);
|
||||
|
||||
document.addEventListener("DOMContentLoaded", initialize);
|
||||
window.CognitiveAgentAssistant = {
|
||||
submit: submitAssistant,
|
||||
|
||||
@@ -13,20 +13,20 @@
|
||||
<link rel="stylesheet" href="/console-v2.css?v=delivery-state-66" />
|
||||
<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-27" />
|
||||
<link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-28" />
|
||||
<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-131" defer></script>
|
||||
<script src="/app.js?v=dual-seat-132" 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="/self-extension.js?v=gscu-tcp-43" defer></script>
|
||||
<script src="/assistant-chat.js?v=codex-assistant-27" defer></script>
|
||||
<script src="/assistant-chat.js?v=codex-assistant-28" defer></script>
|
||||
<script src="/energy-monitor.js?v=energy-monitor-4" defer></script>
|
||||
</head>
|
||||
<body class="console-v2">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const CACHE_VERSION = "cognitive-agent-pad-v278";
|
||||
const CACHE_VERSION = "cognitive-agent-pad-v279";
|
||||
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-131",
|
||||
"/app.js?v=dual-seat-132",
|
||||
"/gesture-control.js?v=android-pad-9",
|
||||
"/pwa.js?v=android-pad-3",
|
||||
"/console-v2.css?v=delivery-state-66",
|
||||
@@ -16,8 +16,8 @@ const APP_SHELL = [
|
||||
"/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-27",
|
||||
"/assistant-chat.js?v=codex-assistant-27",
|
||||
"/assistant-chat.css?v=codex-assistant-28",
|
||||
"/assistant-chat.js?v=codex-assistant-28",
|
||||
"/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