feat: complete dual-seat whole-vehicle orchestration
This commit is contained in:
@@ -151,47 +151,121 @@
|
||||
},
|
||||
};
|
||||
|
||||
const steppedActions = (values, makeAction) => values.map(makeAction);
|
||||
const percentSteps = [0, 25, 50, 75, 100];
|
||||
|
||||
const VEHICLE_DEVICES = [
|
||||
{ name: "主驾座椅", code: "SEAT · CH1 BODY CAN", icon: "armchair", actions: [
|
||||
{ name: "通风低档", command: "seat driver vent low" },
|
||||
{ name: "通风高档", command: "seat driver vent high" },
|
||||
{ name: "关闭通风", command: "seat driver vent off" },
|
||||
{ name: "加热低档", command: "seat driver heat low" },
|
||||
{ name: "加热高档", command: "seat driver heat high" },
|
||||
{ name: "关闭加热", command: "seat driver heat off" },
|
||||
{ name: "按摩弱档", command: "seat driver massage strength weak" },
|
||||
{ name: "舒缓按摩", command: "seat driver massage mode relax" },
|
||||
{ name: "关闭按摩", command: "seat driver massage off" },
|
||||
] },
|
||||
{ name: "空调", code: "CLIMATE · CAN", icon: "snowflake", actions: [
|
||||
{ name: "开启空调", command: "ac on" },
|
||||
{ name: "关闭空调", command: "ac off" },
|
||||
{ name: "24℃", command: "ac temp driver 24" },
|
||||
{ name: "风量 3", command: "ac fan 3" },
|
||||
{ name: "除霜出风", command: "ac mode defrost" },
|
||||
{ name: "自动模式", command: "ac control auto" },
|
||||
] },
|
||||
{ name: "遮阳帘", code: "ROOF · SHADE", icon: "panels-top-left", actions: [
|
||||
{ name: "开启 10%", command: "shade position 10" },
|
||||
{ name: "打开", command: "shade position 100" },
|
||||
{ name: "关闭", command: "shade position 0" },
|
||||
{ name: "半开", command: "shade position 50" },
|
||||
] },
|
||||
{ name: "车内灯光", code: "INTERIOR · LIGHT", icon: "lamp-ceiling", actions: [
|
||||
{ name: "室内灯", command: "light indoor on" },
|
||||
{ name: "关闭室内灯", command: "light indoor off" },
|
||||
{ name: "阅读灯", command: "light reading all on" },
|
||||
{ name: "关闭阅读灯", command: "light reading all off" },
|
||||
{ name: "氛围灯", command: "light ambient on" },
|
||||
{ name: "关闭氛围灯", command: "light ambient off" },
|
||||
{ name: "氛围色号 32", command: "light ambient color 32" },
|
||||
{ name: "氛围亮度", command: "light ambient brightness 3" },
|
||||
] },
|
||||
{ name: "车窗", code: "WINDOW · CH1", icon: "gallery-vertical-end", actions: [
|
||||
{ name: "左前窗 10%", command: "window front-left position 10" },
|
||||
{ name: "左前窗一半", command: "window front-left position 50" },
|
||||
{ name: "左前窗关闭", command: "window front-left position 0" },
|
||||
] },
|
||||
{
|
||||
id: "passenger-seat",
|
||||
name: "副驾座椅",
|
||||
code: "SEAT · CH0 T2G CAN",
|
||||
channel: "CH0",
|
||||
icon: "armchair",
|
||||
parameters: ["通风 0–3 档", "加热 0–2 档", "水平 0–100%", "坐盆 0–100%", "靠背 95–163°", "腿托 95–167°"],
|
||||
actions: [
|
||||
...steppedActions([1, 2, 3], (level) => ({ name: `副驾通风 ${level} 档`, command: `副驾座椅通风${level}档` })),
|
||||
{ name: "关闭副驾通风", command: "关闭副驾座椅通风" },
|
||||
...steppedActions([1, 2], (level) => ({ name: `副驾加热 ${level} 档`, command: `副驾座椅加热${level}档` })),
|
||||
{ name: "关闭副驾加热", command: "关闭副驾座椅加热" },
|
||||
...["水平", "坐盆", "靠背", "腿托"].flatMap((axis) => steppedActions(percentSteps, (position) => ({
|
||||
name: `副驾${axis} ${position}%`,
|
||||
command: `副驾${axis}调到${position}%`,
|
||||
}))),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "driver-seat",
|
||||
name: "主驾座椅",
|
||||
code: "SEAT · CH1 BODY CAN",
|
||||
channel: "CH1",
|
||||
icon: "armchair",
|
||||
parameters: ["通风 / 加热", "按摩强度 / 模式", "水平前后", "整椅高低", "靠背前后", "坐盆高低"],
|
||||
actions: [
|
||||
{ name: "通风低档", command: "seat driver vent low" },
|
||||
{ name: "通风高档", command: "seat driver vent high" },
|
||||
{ name: "关闭通风", command: "seat driver vent off" },
|
||||
{ name: "加热低档", command: "seat driver heat low" },
|
||||
{ name: "加热高档", command: "seat driver heat high" },
|
||||
{ name: "关闭加热", command: "seat driver heat off" },
|
||||
...steppedActions(["weak", "medium", "strong"], (strength) => ({ name: `按摩 ${strength}`, command: `seat driver massage strength ${strength}` })),
|
||||
...steppedActions(["wave", "pulse", "stretch", "slack", "relax"], (mode) => ({ name: `按摩 ${mode}`, command: `seat driver massage mode ${mode}` })),
|
||||
{ name: "关闭按摩", command: "seat driver massage off" },
|
||||
...["horizontal", "backrest"].flatMap((axis) => ["forward", "backward", "stop"].map((direction) => ({
|
||||
name: `${axis} ${direction}`,
|
||||
command: `seat driver ${axis} ${direction}${direction === "stop" ? "" : " 500"}`,
|
||||
}))),
|
||||
...["height", "basin"].flatMap((axis) => ["up", "down", "stop"].map((direction) => ({
|
||||
name: `${axis} ${direction}`,
|
||||
command: `seat driver ${axis} ${direction}${direction === "stop" ? "" : " 500"}`,
|
||||
}))),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "climate",
|
||||
name: "空调",
|
||||
code: "CLIMATE · CH1 CAN",
|
||||
channel: "CH1",
|
||||
icon: "snowflake",
|
||||
parameters: ["电源", "主 / 副驾 18–32℃", "风量 1–7", "出风模式", "内外循环", "前除霜"],
|
||||
actions: [
|
||||
{ name: "开启空调", command: "ac on" },
|
||||
{ name: "关闭空调", command: "ac off" },
|
||||
...["driver", "passenger"].flatMap((zone) => steppedActions(
|
||||
Array.from({ length: 15 }, (_, index) => index + 18),
|
||||
(temperature) => ({ name: `${zone} ${temperature}℃`, command: `ac temp ${zone} ${temperature}` }),
|
||||
)),
|
||||
...steppedActions(Array.from({ length: 7 }, (_, index) => index + 1), (level) => ({ name: `风量 ${level}`, command: `ac fan ${level}` })),
|
||||
...steppedActions(["face", "feet", "face-feet", "defrost", "feet-defrost"], (mode) => ({ name: `出风 ${mode}`, command: `ac mode ${mode}` })),
|
||||
{ name: "自动模式", command: "ac control auto" },
|
||||
{ name: "手动模式", command: "ac control manual" },
|
||||
{ name: "内循环", command: "ac cycle internal" },
|
||||
{ name: "外循环", command: "ac cycle external" },
|
||||
{ name: "开启前除霜", command: "ac defrost on" },
|
||||
{ name: "关闭前除霜", command: "ac defrost off" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "roof",
|
||||
name: "遮阳帘",
|
||||
code: "ROOF · CH1 CAN",
|
||||
channel: "CH1",
|
||||
icon: "panels-top-left",
|
||||
parameters: ["位置 0–100%", "开启", "关闭", "暂停"],
|
||||
actions: steppedActions(Array.from({ length: 11 }, (_, index) => index * 10), (position) => ({
|
||||
name: `遮阳帘 ${position}%`,
|
||||
command: `shade position ${position}`,
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "interior-light",
|
||||
name: "车内灯光",
|
||||
code: "INTERIOR · CH1 CAN",
|
||||
channel: "CH1",
|
||||
icon: "lamp-ceiling",
|
||||
parameters: ["室内灯", "四区阅读灯", "氛围灯电源", "亮度 1–5", "色号 1–127"],
|
||||
actions: [
|
||||
{ name: "室内灯开启", command: "light indoor on" },
|
||||
{ name: "室内灯关闭", command: "light indoor off" },
|
||||
{ name: "阅读灯开启", command: "light reading all on" },
|
||||
{ name: "阅读灯关闭", command: "light reading all off" },
|
||||
{ name: "氛围灯开启", command: "light ambient on" },
|
||||
{ name: "氛围灯关闭", command: "light ambient off" },
|
||||
...steppedActions([1, 2, 3, 4, 5], (level) => ({ name: `氛围亮度 ${level}`, command: `light ambient brightness ${level}` })),
|
||||
...steppedActions([1, 16, 32, 48, 64, 80, 96, 112, 127], (color) => ({ name: `氛围色号 ${color}`, command: `light ambient color ${color}` })),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "windows",
|
||||
name: "车窗",
|
||||
code: "WINDOW · CH1 CAN",
|
||||
channel: "CH1",
|
||||
icon: "gallery-vertical-end",
|
||||
parameters: ["左前 0–100%", "右前 0–100%", "左后 0–100%", "右后 0–100%", "开 / 关 / 停"],
|
||||
actions: ["front-left", "front-right", "rear-left", "rear-right"].flatMap((positionName) => steppedActions(percentSteps, (position) => ({
|
||||
name: `${positionName} ${position}%`,
|
||||
command: `window ${positionName} position ${position}`,
|
||||
}))),
|
||||
},
|
||||
];
|
||||
|
||||
const DEFAULT_DEVICE = {
|
||||
@@ -214,6 +288,8 @@
|
||||
let healthPollTimer = null;
|
||||
let healthToastRequested = false;
|
||||
let latestVehicleStateTurn = -1;
|
||||
let sharedTwinBand = null;
|
||||
let sharedTwinHome = null;
|
||||
|
||||
function uid(prefix) {
|
||||
if (globalThis.crypto?.randomUUID) return `${prefix}-${crypto.randomUUID()}`;
|
||||
@@ -254,6 +330,7 @@
|
||||
settings: { actionDelayMs: 520 },
|
||||
history: [],
|
||||
lastProtocol: null,
|
||||
vehicleExecutions: {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -265,6 +342,9 @@
|
||||
}
|
||||
saved.capabilities = Array.isArray(saved.capabilities) ? saved.capabilities : [];
|
||||
saved.history = Array.isArray(saved.history) ? saved.history : [];
|
||||
saved.vehicleExecutions = saved.vehicleExecutions && typeof saved.vehicleExecutions === "object"
|
||||
? saved.vehicleExecutions
|
||||
: {};
|
||||
saved.selectedDeviceByType = saved.selectedDeviceByType || {};
|
||||
saved.settings = { actionDelayMs: Number(saved.settings?.actionDelayMs) || 520 };
|
||||
return saved;
|
||||
@@ -417,8 +497,17 @@
|
||||
<div class="v2-extension-vehicle-grid" id="extensionVehicleGrid"></div>
|
||||
</section>
|
||||
</div>
|
||||
<section class="v2-extension-twin-board" aria-labelledby="extensionTwinTitle">
|
||||
<header>
|
||||
<div><span class="section-kicker">SHARED VEHICLE DIGITAL TWIN</span><h2 id="extensionTwinTitle">整车孪生实时映射</h2><p>与自编排复用同一孪生实例,CH0 副驾座椅与 CH1 主驾座椅 / 车身状态持续同步。</p></div>
|
||||
<span><i></i>实际反馈 / 当前动作</span>
|
||||
</header>
|
||||
<div class="v2-extension-twin-host" id="extensionTwinHost">
|
||||
<div class="v2-extension-twin-waiting"><i data-lucide="loader-circle"></i><span>正在挂载整车孪生界面</span></div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="v2-extension-run-plan" id="extensionVehicleStateSummary" data-state="unknown">
|
||||
<span><i>CH1</i>等待权威车端状态</span>
|
||||
<span><i>CH0 / CH1</i>等待权威车端状态</span>
|
||||
</div>
|
||||
<div class="v2-extension-run-plan" id="extensionRunPlan" hidden></div>
|
||||
</section>`;
|
||||
@@ -527,9 +616,13 @@
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
function isPhysicalVehicleSource(value) {
|
||||
return ["physical_can", "physical_can_ch0", "physical_can_ch1", "physical_partial"].includes(value);
|
||||
}
|
||||
|
||||
function containsAuthoritativePhysicalCh1State(value) {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
if (value.source === "physical_can_ch1" || value.feedback_source === "physical_can_ch1") {
|
||||
if (isPhysicalVehicleSource(value.source) || isPhysicalVehicleSource(value.feedback_source)) {
|
||||
return true;
|
||||
}
|
||||
return Object.values(value).some((item) => containsAuthoritativePhysicalCh1State(item));
|
||||
@@ -551,21 +644,37 @@
|
||||
if (!summary) return false;
|
||||
|
||||
const requestLabel = String(payload.request_id || "-").slice(-8);
|
||||
const hasCh0 = containsAuthoritativePhysicalCh1State(payload.actuator_state?.passenger);
|
||||
const hasCh1 = containsAuthoritativePhysicalCh1State(payload.actuator_state?.driver)
|
||||
|| containsAuthoritativePhysicalCh1State(payload.domain_state);
|
||||
const channelLabel = hasCh0 && hasCh1 ? "CH0 + CH1" : hasCh0 ? "CH0" : "CH1";
|
||||
const items = [
|
||||
`<span class="done"><i>CH1</i>真实车况 · TURN ${Number.isFinite(turn) ? turn : "-"} · ${escapeHtml(requestLabel)}</span>`,
|
||||
`<span class="done"><i>${channelLabel}</i>真实车况 · TURN ${Number.isFinite(turn) ? turn : "-"} · ${escapeHtml(requestLabel)}</span>`,
|
||||
];
|
||||
const feedback = Array.isArray(payload.physical_feedback)
|
||||
? payload.physical_feedback.filter((item) => (
|
||||
item?.confirmed === true
|
||||
&& item?.channel === 1
|
||||
&& item?.source === "physical_can_ch1"
|
||||
&& [0, 1].includes(Number(item?.channel))
|
||||
&& isPhysicalVehicleSource(item?.source)
|
||||
))
|
||||
: [];
|
||||
if (feedback.length) {
|
||||
const frames = [...new Set(feedback.map((item) => item.frame?.frame_id).filter(Boolean))];
|
||||
items.push(`<span><i>FB</i>${feedback.length}/${payload.physical_feedback.length} MATCH · ${escapeHtml(frames.join(" / ") || "CH1")}</span>`);
|
||||
items.push(`<span><i>FB</i>${feedback.length}/${payload.physical_feedback.length} MATCH · ${escapeHtml(frames.join(" / ") || channelLabel)}</span>`);
|
||||
}
|
||||
const driver = payload.actuator_state?.driver || {};
|
||||
const passenger = payload.actuator_state?.passenger || {};
|
||||
if (isPhysicalVehicleSource(passenger.ventilation?.feedback_source)) {
|
||||
items.push(`<span><i>P-VENT</i>${passenger.ventilation.state === "on" ? `ON L${escapeHtml(passenger.ventilation.level)}` : "OFF"}</span>`);
|
||||
}
|
||||
if (isPhysicalVehicleSource(passenger.heating?.feedback_source)) {
|
||||
items.push(`<span><i>P-HEAT</i>${passenger.heating.state === "on" ? `ON L${escapeHtml(passenger.heating.level)}` : "OFF"}</span>`);
|
||||
}
|
||||
const passengerPosition = passenger.position || {};
|
||||
Object.entries(passengerPosition).forEach(([axis, value]) => {
|
||||
if (!isPhysicalVehicleSource(value?.feedback_source)) return;
|
||||
items.push(`<span><i>P-SEAT</i>${escapeHtml(axis)} ${escapeHtml(value.position_pct ?? "-")}%</span>`);
|
||||
});
|
||||
const ventilation = driver.ventilation;
|
||||
const heating = driver.heating;
|
||||
if (ventilation?.feedback_source === "physical_can_ch1") {
|
||||
@@ -602,11 +711,33 @@
|
||||
const color = `${ambientLight.front_color_index ?? "-"}/${ambientLight.rear_color_index ?? "-"}`;
|
||||
items.push(`<span><i>AMBIENT</i>${escapeHtml(ambientLight.power || "unknown")} · L${escapeHtml(brightness)} · C${escapeHtml(color)}</span>`);
|
||||
}
|
||||
summary.dataset.state = "physical_can_ch1";
|
||||
summary.dataset.state = hasCh0 && hasCh1 ? "physical_can_dual" : hasCh0 ? "physical_can_ch0" : "physical_can_ch1";
|
||||
summary.innerHTML = items.join("");
|
||||
return true;
|
||||
}
|
||||
|
||||
function syncSharedTwinMount() {
|
||||
const host = $("#extensionTwinHost");
|
||||
const shouldMount = document.body.dataset.module === "self-extension"
|
||||
&& activeMode === "control";
|
||||
if (!sharedTwinBand) {
|
||||
sharedTwinBand = $("#seatTwin")?.closest(".twin-band") || null;
|
||||
if (sharedTwinBand?.parentNode) {
|
||||
sharedTwinHome = document.createComment("shared-vehicle-twin-home");
|
||||
sharedTwinBand.parentNode.insertBefore(sharedTwinHome, sharedTwinBand);
|
||||
}
|
||||
}
|
||||
if (!sharedTwinBand || !sharedTwinHome || !host) return;
|
||||
if (shouldMount) {
|
||||
if (sharedTwinBand.parentNode !== host) host.replaceChildren(sharedTwinBand);
|
||||
sharedTwinBand.classList.add("v2-extension-shared-twin");
|
||||
} else if (sharedTwinHome.parentNode && sharedTwinBand.parentNode !== sharedTwinHome.parentNode) {
|
||||
sharedTwinHome.parentNode.insertBefore(sharedTwinBand, sharedTwinHome.nextSibling);
|
||||
sharedTwinBand.classList.remove("v2-extension-shared-twin");
|
||||
}
|
||||
window.setTimeout(() => window.dispatchEvent(new Event("resize")), 80);
|
||||
}
|
||||
|
||||
function setMode(mode) {
|
||||
activeMode = mode === "control" ? "control" : "study";
|
||||
sessionStorage.setItem(MODE_KEY, activeMode);
|
||||
@@ -620,6 +751,7 @@
|
||||
view.hidden = view.dataset.extensionView !== activeMode;
|
||||
});
|
||||
if (activeMode === "control") renderControl();
|
||||
syncSharedTwinMount();
|
||||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||||
}
|
||||
|
||||
@@ -687,8 +819,70 @@
|
||||
return `<button class="v2-extension-learned-action" type="button" data-run-extension-capability="${escapeHtml(capability.id)}" data-extension-type="${escapeHtml(device?.type || "other")}"><span><i data-lucide="${type.icon}"></i></span><div><small>${escapeHtml(device?.name || type.label)}</small><strong>${escapeHtml(capability.name)}</strong></div><i data-lucide="send"></i></button>`;
|
||||
}).join("");
|
||||
}
|
||||
$("#extensionVehicleGrid").innerHTML = VEHICLE_DEVICES.map((device) => `
|
||||
<article class="v2-extension-vehicle-card"><header><span><i data-lucide="${device.icon}"></i></span><div><strong>${device.name}</strong><small>${device.code}</small></div></header><div>${device.actions.map((action) => `<button type="button" data-extension-vehicle-command="${escapeHtml(action.command)}" title="${escapeHtml(action.command)}">${escapeHtml(action.name)}</button>`).join("")}</div></article>`).join("");
|
||||
$("#extensionVehicleGrid").innerHTML = VEHICLE_DEVICES.map((device) => {
|
||||
const execution = state.vehicleExecutions?.[device.id];
|
||||
const executionState = execution?.state || "idle";
|
||||
return `
|
||||
<article class="v2-extension-vehicle-card" data-extension-vehicle-device="${escapeHtml(device.id)}" data-execution-state="${escapeHtml(executionState)}">
|
||||
<header>
|
||||
<span><i data-lucide="${device.icon}"></i></span>
|
||||
<div><strong>${escapeHtml(device.name)}</strong><small>${escapeHtml(device.code)}</small></div>
|
||||
<b>${escapeHtml(device.channel)}</b>
|
||||
</header>
|
||||
<div class="v2-extension-parameter-list" aria-label="${escapeHtml(device.name)}可控参数">
|
||||
${device.parameters.map((parameter) => `<span>${escapeHtml(parameter)}</span>`).join("")}
|
||||
</div>
|
||||
<footer>
|
||||
<small>当前动作</small>
|
||||
<strong data-extension-current-action>${escapeHtml(execution?.action || "等待指令")}</strong>
|
||||
<span data-extension-action-detail>${escapeHtml(execution?.detail || "输入自然语言后在此显示执行结果")}</span>
|
||||
</footer>
|
||||
</article>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function findVehicleAction(command) {
|
||||
for (const device of VEHICLE_DEVICES) {
|
||||
const action = device.actions.find((item) => item.command === command);
|
||||
if (action) return { device, action };
|
||||
}
|
||||
const inferredDeviceId = /^副驾/.test(command)
|
||||
? "passenger-seat"
|
||||
: /^seat driver /.test(command)
|
||||
? "driver-seat"
|
||||
: /^ac /.test(command)
|
||||
? "climate"
|
||||
: /^shade /.test(command)
|
||||
? "roof"
|
||||
: /^light /.test(command)
|
||||
? "interior-light"
|
||||
: /^window /.test(command)
|
||||
? "windows"
|
||||
: null;
|
||||
const device = VEHICLE_DEVICES.find((item) => item.id === inferredDeviceId);
|
||||
if (device) return { device, action: { name: command, command } };
|
||||
return null;
|
||||
}
|
||||
|
||||
function updateVehicleCardExecution(command, executionState, detail) {
|
||||
const match = findVehicleAction(command);
|
||||
if (!match) return;
|
||||
const actionText = match.action.name || command;
|
||||
state.vehicleExecutions ||= {};
|
||||
state.vehicleExecutions[match.device.id] = {
|
||||
state: executionState,
|
||||
action: actionText,
|
||||
detail,
|
||||
command,
|
||||
at: new Date().toISOString(),
|
||||
};
|
||||
const card = $(`[data-extension-vehicle-device="${match.device.id}"]`);
|
||||
if (!card) return;
|
||||
card.dataset.executionState = executionState;
|
||||
const actionNode = card.querySelector("[data-extension-current-action]");
|
||||
const detailNode = card.querySelector("[data-extension-action-detail]");
|
||||
if (actionNode) actionNode.textContent = actionText;
|
||||
if (detailNode) detailNode.textContent = detail;
|
||||
}
|
||||
|
||||
function renderTypeChooser() {
|
||||
@@ -970,8 +1164,11 @@
|
||||
|
||||
function vehicleCatalog() {
|
||||
return VEHICLE_DEVICES.map((device) => ({
|
||||
id: device.id,
|
||||
name: device.name,
|
||||
code: device.code,
|
||||
channel: device.channel,
|
||||
parameters: device.parameters,
|
||||
actions: device.actions,
|
||||
}));
|
||||
}
|
||||
@@ -1155,17 +1352,20 @@
|
||||
conversationId: result.conversation_id || null,
|
||||
conversationTurn: result.conversation_turn ?? null,
|
||||
};
|
||||
saveState();
|
||||
renderAuthoritativeVehicleState(result);
|
||||
$$('[data-extension-vehicle-command]').forEach((button) => {
|
||||
if (button.dataset.extensionVehicleCommand !== command) return;
|
||||
button.dataset.executionState = completed ? "succeeded" : "failed";
|
||||
button.setAttribute(
|
||||
"aria-label",
|
||||
`${button.textContent.trim()}:${completed ? "执行成功" : "执行未完成"}`,
|
||||
);
|
||||
button.title = `${command} · ${result.message || result.status || "执行结果未知"}`;
|
||||
});
|
||||
const operationCount = Array.isArray(result.operation_results)
|
||||
? result.operation_results.length
|
||||
: 0;
|
||||
const physicalCount = Array.isArray(result.physical_feedback)
|
||||
? result.physical_feedback.filter((item) => item?.confirmed === true).length
|
||||
: 0;
|
||||
const detail = completed
|
||||
? result.execution_mode === "hardware"
|
||||
? `物理回读已确认 · ${physicalCount || operationCount} 项`
|
||||
: `安全演练已完成 · ${operationCount || 1} 项`
|
||||
: vehicleExecutionError(result);
|
||||
updateVehicleCardExecution(command, completed ? "succeeded" : "failed", detail);
|
||||
saveState();
|
||||
window.dispatchEvent(new CustomEvent("cognitive-agent:extension-vehicle-result", {
|
||||
detail: result,
|
||||
}));
|
||||
@@ -1186,7 +1386,7 @@
|
||||
result = await api("vehicle/execute", {
|
||||
method: "POST",
|
||||
// Channel, seat target, provider, vehicle profile and execution mode
|
||||
// are intentionally absent: those are server-side CH1 safety policy.
|
||||
// are intentionally absent: the server owns CH0/CH1 routing and safety policy.
|
||||
body: JSON.stringify({ command, conversation_id: sharedConversationId() }),
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -1222,6 +1422,8 @@
|
||||
if (step.kind === "delay") {
|
||||
await delay(step.delayMs);
|
||||
} else if (step.kind === "vehicle") {
|
||||
updateVehicleCardExecution(step.command, "running", `正在执行:${step.label || step.command}`);
|
||||
saveState();
|
||||
const result = await executeVehicle(step.command);
|
||||
vehicleDryRun ||= result.execution_mode === "dry_run";
|
||||
if (result.execution_mode === "hardware") {
|
||||
@@ -1473,12 +1675,6 @@
|
||||
const run = event.target.closest("[data-run-extension-capability]");
|
||||
if (run) runCapability(run.dataset.runExtensionCapability);
|
||||
});
|
||||
$("#extensionVehicleGrid").addEventListener("click", (event) => {
|
||||
const action = event.target.closest("[data-extension-vehicle-command]");
|
||||
if (!action) return;
|
||||
$("#extensionUtterance").value = action.dataset.extensionVehicleCommand;
|
||||
executeInstruction(action.dataset.extensionVehicleCommand);
|
||||
});
|
||||
$("#extensionExecuteButton").addEventListener(
|
||||
"click",
|
||||
() => executeInstruction($("#extensionUtterance").value),
|
||||
@@ -1510,6 +1706,8 @@
|
||||
if (event.detail?.module === "self-extension") {
|
||||
setMode(activeMode);
|
||||
refreshHealth();
|
||||
} else {
|
||||
syncSharedTwinMount();
|
||||
}
|
||||
});
|
||||
window.addEventListener("cognitive-agent:result", (event) => {
|
||||
|
||||
Reference in New Issue
Block a user