完善模式卡片、NFC及多设备控制流程

This commit is contained in:
minghanchen
2026-09-01 16:18:57 +08:00
parent ca26259e63
commit cbf230182d
15 changed files with 1092 additions and 191 deletions
+221 -108
View File
@@ -4949,9 +4949,6 @@ function modeEditorFlowFromResult(result) {
const functionConfigs = result?.function_configs && typeof result.function_configs === "object"
? result.function_configs
: {};
const executionSteps = Array.isArray(result?.execution_report?.spec?.results)
? result.execution_report.spec.results
: [];
return {
schema: "cognitive-agent-mode-command-flow/v1",
source_request_id: result?.request_id || "",
@@ -4959,7 +4956,7 @@ function modeEditorFlowFromResult(result) {
control_transport: result?.control_transport || "",
function_configs: functionConfigs,
plans,
execution_steps: executionSteps,
execution_steps: [],
};
}
@@ -5283,7 +5280,7 @@ function renderModeEditorFlow() {
const empty = document.createElement("p");
empty.textContent = commandCount > 0
? "已生成 CAN 控制流,但当前指令没有可读映射。"
: "点击“执行”后,这里会显示由后端生成的 CAN 控制指令流。";
: "点击“生成指令”后,这里会显示由后端解析、校验并编译的控制指令流。";
preview.append(empty);
}
}
@@ -5299,9 +5296,9 @@ function setModeEditorFlowExpanded(expanded) {
button.setAttribute("aria-expanded", String(nextExpanded));
button.setAttribute(
"aria-label",
nextExpanded ? "还原 CAN 控制指令区域" : "展开 CAN 控制指令区域",
nextExpanded ? "还原控制指令区域" : "展开控制指令区域",
);
button.title = nextExpanded ? "还原 CAN 控制指令区域" : "展开 CAN 控制指令区域";
button.title = nextExpanded ? "还原控制指令区域" : "展开控制指令区域";
button.innerHTML = `<i data-lucide="${nextExpanded ? "minimize-2" : "maximize-2"}"></i>`;
refreshIcons();
}
@@ -5328,8 +5325,8 @@ function openModeEditor(modeProfileId) {
$("#modeEditorName").value = mode.name || "";
$("#modeEditorInstruction").value = instruction;
$("#modeEditorStatus").textContent = flow
? "已载入永久保存的自然语言和 CAN 指令流,可修改后重新执行。"
: "修改自然语言后需要重新执行,才能点击完成永久保存。";
? "已载入永久保存的自然语言和控制指令流,可修改后重新生成。"
: "修改自然语言后需要重新生成指令,才能点击完成永久保存。";
setModeEditorFlowExpanded(false);
backdrop.hidden = false;
document.body.classList.add("mode-editor-open");
@@ -5360,8 +5357,8 @@ function invalidateModeEditorFlow() {
const instruction = $("#modeEditorInstruction").value.trim();
const current = instruction === state.modeEditor.executedInstruction;
$("#modeEditorStatus").textContent = current && modeEditorCommandCount(state.modeEditor.flow) > 0
? "当前自然语言已有 CAN 指令流,可点击完成永久保存。"
: "自然语言已修改,请重新点击执行生成 CAN 控制指令流。";
? "当前自然语言已有控制指令流,可点击完成永久保存。"
: "自然语言已修改,请重新点击“生成指令”。";
renderModeEditorFlow();
}
@@ -5376,38 +5373,37 @@ async function executeModeEditorInstruction() {
const execute = $("#modeEditorExecute");
execute.disabled = true;
execute.classList.add("loading");
execute.innerHTML = '<i data-lucide="loader-circle"></i><span>执行中…</span>';
execute.innerHTML = '<i data-lucide="loader-circle"></i><span>生成中…</span>';
$("#modeEditorComplete").disabled = true;
$("#modeEditorStatus").textContent = "正在解析自然语言并生成、执行 CAN 控制指令流…";
$("#modeEditorStatus").textContent = "正在复用主页解析流程生成控制指令;不会执行设备…";
refreshIcons();
try {
const execution = await requestCommandAndWait(instruction);
const result = execution.result;
const { payload: result } = await requestNaturalLanguageControl(instruction, {
executionMode: "dry_run",
clientType: "mode_editor_compile",
previewOnly: true,
});
const flow = modeEditorFlowFromResult(result);
const commandCount = modeEditorCommandCount(flow);
if (!result || commandCount === 0) {
throw new Error(execution.error?.message || "未生成可保存的 CAN 控制指令流");
throw new Error("未生成可保存的控制指令流");
}
editor.flow = flow;
editor.sourceRequestId = result.request_id;
editor.executedInstruction = instruction;
$("#modeEditorStatus").textContent = `已生成 ${commandCount} 条 CAN 控制指令;执行状态:${result.status}。`;
setModeExecutionState(
editor.modeId,
["succeeded", "partially_succeeded"].includes(result.status) ? "succeeded" : "failed",
);
$("#modeEditorStatus").textContent = `已生成 ${commandCount} 条控制指令;尚未执行设备。点击“完成”保存到模式卡片。`;
renderModeEditorFlow();
} catch (error) {
editor.flow = null;
editor.sourceRequestId = "";
editor.executedInstruction = "";
$("#modeEditorStatus").textContent = error.message || "CAN 控制指令流生成失败。";
$("#modeEditorStatus").textContent = error.message || "控制指令流生成失败。";
renderModeEditorFlow();
showToast(error.message || "CAN 控制指令流生成失败");
showToast(error.message || "控制指令流生成失败");
} finally {
execute.disabled = false;
execute.classList.remove("loading");
execute.innerHTML = '<i data-lucide="play"></i><span>执行</span>';
execute.innerHTML = '<i data-lucide="wand-sparkles"></i><span>生成指令</span>';
refreshIcons();
}
}
@@ -5426,7 +5422,7 @@ async function completeModeEditor() {
|| editor.executedInstruction !== instruction
|| modeEditorCommandCount(editor.flow) === 0
) {
showToast("请先执行当前自然语言并生成 CAN 控制指令流");
showToast("请先根据当前自然语言生成控制指令流");
return;
}
const complete = $("#modeEditorComplete");
@@ -5588,7 +5584,7 @@ async function deleteUserModeProfile(modeProfileId) {
if (!mode || !isUserModeProfile(mode)) return;
const presetOverride = mode.preset_override === true;
const confirmation = presetOverride
? `恢复“${mode.name}”的初始预设配置?当前保存的名称、自然语言和 CAN 指令流将被删除。`
? `恢复“${mode.name}”的初始预设配置?当前保存的名称、自然语言和控制指令流将被删除。`
: `删除自定义功能模式“${mode.name}”?删除后无法恢复。`;
if (!window.confirm(confirmation)) return;
@@ -5655,12 +5651,83 @@ function setModeChipsDisabled(disabled) {
});
}
async function executeSavedModeProfile(mode) {
if (!mode?.id || !mode?.command_flow?.source_request_id) return false;
const driverOnlyCh1 = isDriverOnlyCh1Runtime();
if (state.executionMode === "hardware"
&& state.controlTransport !== "wireless_mqtt"
&& !driverOnlyCh1
&& !isLocalDecisionHost()
&& state.hardwareAccess?.state !== "approved") {
showToast(state.hardwareAccess?.state === "pending"
? "申请仍在等待本地审批"
: "请先申请并获得本地真实台架使用权");
return true;
}
closeModeGallery({ restoreFocus: false });
const runButton = $("#runButton");
runButton.disabled = true;
setModeChipsDisabled(true);
setModeExecutionState(mode.id, "running");
$("#utterance").value = mode.name || mode.id;
setRunStatus("processing", "MODE COMMAND FLOW");
$("#resultTitle").textContent = `正在直接执行${mode.name}`;
try {
const payload = await postControlPhase(
`/api/mode-profiles/${encodeURIComponent(mode.id)}/execute`,
{
client_type: "web_console_mode_card",
conversation_id: state.conversationId,
vehicle_id: state.vehicleId || state.runtime?.default_vehicle_id,
execution_mode: state.executionMode,
control_transport: state.controlTransport,
...(state.executionMode === "hardware"
&& state.controlTransport !== "wireless_mqtt"
&& !driverOnlyCh1
? { hardware_client_id: state.hardwareClientId }
: {}),
...(state.executionMode === "simulated_hardware"
? { simulation_fault: $("#simulationFault").value }
: {}),
},
state.executionMode,
);
if (!payload) throw new Error("模式卡片未返回执行结果");
renderResult(payload);
const completed = ["succeeded", "partially_succeeded"].includes(payload.status);
$("#utterance").value = mode.name || mode.id;
$("#resultTitle").textContent = completed
? `${mode.name}执行完成`
: `${mode.name}执行未完成`;
setModeExecutionState(mode.id, completed ? "succeeded" : "failed");
if (state.executionMode === "hardware") await synchronizePhysicalSeatState();
showToast(completed
? `${mode.name}已直接执行保存的控制指令流`
: `${mode.name}未完成,请查看执行结果`);
} catch (error) {
setModeExecutionState(mode.id, "failed");
setRunStatus("failed", "MODE FAILED");
$("#resultTitle").textContent = `${mode.name}执行失败`;
showToast(error.message || "模式执行失败");
} finally {
runButton.disabled = false;
setModeChipsDisabled(false);
}
return true;
}
function applyModeCommand(command, modeProfileId = null) {
if (!command) return;
if ($("#runButton").disabled) {
showToast("上一条指令仍在执行,请稍候");
return;
}
const mode = modeProfiles().find((item) => item.id === modeProfileId);
if (mode?.command_flow?.source_request_id) {
void executeSavedModeProfile(mode);
return;
}
closeModeGallery({ restoreFocus: false });
state.pendingModeProfileId = modeProfileId;
if (modeProfileId) setModeExecutionState(modeProfileId, "running");
@@ -7094,7 +7161,8 @@ function renderExecution(result) {
.startsWith("nfc_peripheral");
summary.className = `execution-summary ${submittedUnverified || partiallySucceeded ? "partial" : reportSucceeded ? "succeeded" : "failed"}`;
const presetSave = Boolean(result.preset_config);
const modeSave = Boolean(result.mode_profile);
const savedModeExecution = result.orchestration_route === "saved_mode_command_flow";
const modeSave = Boolean(result.mode_profile) && !savedModeExecution;
const simulatedHil = report.metadata?.labels?.seat_feedback === "simulated_hardware";
const physical = report.spec.mode === "hil" && !simulatedHil;
const executedDomains = [...new Set(report.spec.results.map((item) => item.domain))];
@@ -7379,7 +7447,9 @@ function renderResult(result) {
? `${domains.length} 域场景部分完成`
: executionSucceeded
? result.mode_profile
? "长期模式配置已保存"
? savedModeExecution
? `${result.mode_profile.name}执行完成`
: "长期模式配置已保存"
: result.preset_config
? "座椅姿态预设已保存"
: submittedUnverified
@@ -7481,6 +7551,115 @@ async function postControlPhase(path, body, executionMode, onEvent = null) {
return payload;
}
function naturalLanguageRequestBody(
utterance,
{
executionMode = state.executionMode,
clientType = "web_console",
} = {},
) {
const driverOnlyCh1 = isDriverOnlyCh1Runtime();
return {
utterance,
client_type: clientType,
provider: state.provider,
conversation_id: state.conversationId,
vehicle_id: state.vehicleId || state.runtime?.default_vehicle_id,
execution_mode: executionMode,
control_transport: state.controlTransport,
...(state.provider === "openai" ? { reasoning_effort: state.reasoningEffort } : {}),
...(executionMode === "hardware"
&& state.controlTransport !== "wireless_mqtt"
&& !driverOnlyCh1
? { hardware_client_id: state.hardwareClientId }
: {}),
...(executionMode === "simulated_hardware"
? { simulation_fault: $("#simulationFault").value }
: {}),
};
}
async function requestNaturalLanguageControl(
utterance,
{
executionMode = state.executionMode,
clientType = "web_console",
previewOnly = false,
onControlPhaseEvent = null,
onPreview = null,
onSupplementStart = null,
} = {},
) {
const requestBody = naturalLanguageRequestBody(utterance, {
executionMode,
clientType,
});
const usePreview = previewOnly || state.provider === "openai";
if (!usePreview) {
const payload = await postControlPhase(
"/api/requests",
requestBody,
executionMode,
onControlPhaseEvent,
);
return { payload, supplementWarning: "" };
}
const preview = await postControlPhase(
"/api/requests/preview",
requestBody,
executionMode,
onControlPhaseEvent,
);
if (!preview) return { payload: null, supplementWarning: "" };
if (typeof onPreview === "function") onPreview(preview);
if (!preview.supplement_required) {
return { payload: preview, supplementWarning: "" };
}
if (typeof onSupplementStart === "function") onSupplementStart(preview);
try {
const payload = await postControlPhase(
`/api/requests/${preview.request_id}/supplement`,
{
client_type: clientType,
provider: state.provider,
reasoning_effort: state.reasoningEffort,
...(executionMode === "hardware"
&& state.controlTransport !== "wireless_mqtt"
&& !isDriverOnlyCh1Runtime()
? { hardware_client_id: state.hardwareClientId }
: {}),
},
executionMode,
onControlPhaseEvent,
);
return { payload, supplementWarning: "" };
} catch (error) {
const localCommandCount = Object.values(preview.function_configs || {})
.reduce((total, config) => total + (config?.spec?.operations?.length || 0), 0);
if (!localCommandCount) throw error;
return {
payload: {
...preview,
status: "partially_succeeded",
supplement_required: false,
cloud_supplement_error: {
code: error.code || "cloud_supplement_failed",
message: error.message,
},
decision_summary: [
...(preview.decision_summary || []),
`云端补充失败:${error.message};本地规则命令已保留且不会撤销。`,
],
},
supplementWarning: previewOnly
? "本地规则指令已生成;云端补充失败"
: "本地规则命令已执行;云端补充失败,已保留本地结果",
};
}
}
async function executeCanCommands(canCommands, { deviceName = "车辆设备" } = {}) {
const commands = Array.isArray(canCommands)
? canCommands.map((command) => String(command || "").trim()).filter(Boolean)
@@ -7568,26 +7747,6 @@ async function submitRequest(event) {
setRunStatus("processing", "PROCESSING");
$("#resultTitle").textContent = "正在执行车舱编排";
try {
const requestBody = {
utterance,
client_type: "web_console",
provider: state.provider,
conversation_id: state.conversationId,
vehicle_id: state.vehicleId || state.runtime?.default_vehicle_id,
execution_mode: state.executionMode,
control_transport: state.controlTransport,
...(state.provider === "openai" ? { reasoning_effort: state.reasoningEffort } : {}),
...(state.executionMode === "hardware"
&& state.controlTransport !== "wireless_mqtt"
&& !driverOnlyCh1
? { hardware_client_id: state.hardwareClientId }
: {}),
...(state.executionMode === "simulated_hardware"
? { simulation_fault: $("#simulationFault").value }
: {}),
};
let payload;
let supplementWarning = "";
const onControlPhaseEvent = (phaseEvent) => {
if (phaseEvent.type !== "validated_commands" || !phaseEvent.payload) return;
window.dispatchEvent(new CustomEvent(
@@ -7595,67 +7754,21 @@ async function submitRequest(event) {
{ detail: phaseEvent.payload },
));
};
if (state.provider === "openai") {
const preview = await postControlPhase(
"/api/requests/preview",
requestBody,
state.executionMode,
const { payload, supplementWarning } = await requestNaturalLanguageControl(
utterance,
{
onControlPhaseEvent,
);
if (!preview) return;
renderResult(preview);
completedResult = preview;
if (preview.supplement_required) {
setRunStatus("processing", "CLOUD SUPPLEMENT");
$("#resultTitle").textContent = "本地指令已处理,正在补充云端解析";
try {
payload = await postControlPhase(
`/api/requests/${preview.request_id}/supplement`,
{
client_type: "web_console",
provider: state.provider,
reasoning_effort: state.reasoningEffort,
...(state.executionMode === "hardware"
&& state.controlTransport !== "wireless_mqtt"
&& !driverOnlyCh1
? { hardware_client_id: state.hardwareClientId }
: {}),
},
state.executionMode,
onControlPhaseEvent,
);
if (!payload) return;
} catch (error) {
const localCommandCount = Object.values(preview.function_configs || {})
.reduce((total, config) => total + (config?.spec?.operations?.length || 0), 0);
if (!localCommandCount) throw error;
payload = {
...preview,
status: "partially_succeeded",
supplement_required: false,
cloud_supplement_error: {
code: error.code || "cloud_supplement_failed",
message: error.message,
},
decision_summary: [
...(preview.decision_summary || []),
`云端补充失败:${error.message};本地规则命令已保留且不会撤销。`,
],
};
supplementWarning = "本地规则命令已执行;云端补充失败,已保留本地结果";
}
} else {
payload = preview;
}
} else {
payload = await postControlPhase(
"/api/requests",
requestBody,
state.executionMode,
onControlPhaseEvent,
);
if (!payload) return;
}
onPreview: (preview) => {
renderResult(preview);
completedResult = preview;
},
onSupplementStart: () => {
setRunStatus("processing", "CLOUD SUPPLEMENT");
$("#resultTitle").textContent = "本地指令已处理,正在补充云端解析";
},
},
);
if (!payload) return;
renderResult(payload);
completedResult = payload;
const peripheralRoute = String(payload.orchestration_route || "")