完善模式卡片、NFC及多设备控制流程
This commit is contained in:
+221
-108
@@ -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 || "")
|
||||
|
||||
Reference in New Issue
Block a user