完善模式卡片、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 || "")
+1 -1
View File
@@ -429,7 +429,7 @@
<li data-can-channel="${escapeHtml(item.channel)}" data-delivery-state="${escapeHtml(item.deliveryState)}">
<span>
<b class="v2-can-command-channel">${escapeHtml(item.channel)}</b>
${item.parseSource ? `<b class="v2-can-command-source" data-parse-source="${escapeHtml(item.parseSource)}">${item.parseSource === "direct_control" ? "设备控件" : item.parseSource === "local_rule" ? "本地规则" : "云端模型"}</b>` : ""}
${item.parseSource ? `<b class="v2-can-command-source" data-parse-source="${escapeHtml(item.parseSource)}">${item.parseSource === "direct_control" ? "设备控件" : item.parseSource === "saved_mode_command_flow" ? "模式卡片" : item.parseSource === "local_rule" ? "本地规则" : "云端模型"}</b>` : ""}
<b class="v2-can-command-delivery" data-delivery-state="${escapeHtml(item.deliveryState)}" title="${escapeHtml(`${item.deliveryTransport} · ${item.deliveryLabel}`)}">${escapeHtml(item.deliveryLabel)}</b>
<i class="v2-can-command-category">${escapeHtml(item.category)}</i>
</span>
+8 -8
View File
@@ -17,10 +17,10 @@
<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-107" defer></script>
<script src="/app.js?v=dual-seat-109" 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-49" defer></script>
<script src="/console-v2.js?v=delivery-state-50" defer></script>
<script src="/self-extension.js?v=gscu-tcp-40" defer></script>
</head>
<body class="console-v2">
@@ -574,7 +574,7 @@
<div>
<span>MODE WORKFLOW</span>
<h2 id="modeEditorTitle">编辑功能模式</h2>
<p id="modeEditorDescription">执行后生成 CAN 控制指令流;完成后永久保存到当前模式卡片。</p>
<p id="modeEditorDescription">复用主页自然语言解析流程生成控制指令;此步骤不会执行设备。</p>
</div>
<button id="modeEditorClose" type="button" aria-label="关闭模式编辑器"><i data-lucide="x"></i></button>
</header>
@@ -591,22 +591,22 @@
</div>
<section class="mode-editor-flow" aria-live="polite">
<header>
<span><i data-lucide="workflow"></i>CAN 控制指令流</span>
<span><i data-lucide="workflow"></i>控制指令流</span>
<div class="mode-editor-flow-tools">
<b id="modeEditorFlowCount">尚未生成</b>
<button id="modeEditorFlowExpand" type="button" aria-expanded="false" aria-label="展开 CAN 控制指令区域" title="展开 CAN 控制指令区域">
<button id="modeEditorFlowExpand" type="button" aria-expanded="false" aria-label="展开控制指令区域" title="展开控制指令区域">
<i data-lucide="maximize-2"></i>
</button>
</div>
</header>
<div class="mode-editor-flow-preview empty" id="modeEditorFlowPreview">
<p>点击“执行”后,这里会显示由后端生成的 CAN 控制指令流。</p>
<p>点击“生成指令”后,这里会显示由后端解析、校验并编译的控制指令流。</p>
</div>
</section>
<p class="mode-editor-status" id="modeEditorStatus">修改自然语言后需要重新执行,才能点击完成永久保存。</p>
<p class="mode-editor-status" id="modeEditorStatus">修改自然语言后需要重新生成指令,才能点击完成永久保存。</p>
<footer class="mode-editor-actions">
<button id="modeEditorCancel" type="button">取消</button>
<button class="mode-editor-execute" id="modeEditorExecute" type="button"><i data-lucide="play"></i><span>执行</span></button>
<button class="mode-editor-execute" id="modeEditorExecute" type="button"><i data-lucide="wand-sparkles"></i><span>生成指令</span></button>
<button class="mode-editor-complete" id="modeEditorComplete" type="button" disabled><i data-lucide="save"></i><span>完成</span></button>
</footer>
</section>
+3 -3
View File
@@ -1,4 +1,4 @@
const CACHE_VERSION = "cognitive-agent-pad-v218";
const CACHE_VERSION = "cognitive-agent-pad-v220";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [
@@ -6,11 +6,11 @@ const APP_SHELL = [
"/index.html",
"/styles.css?v=dual-seat-16",
"/network.js?v=android-pad-2",
"/app.js?v=dual-seat-107",
"/app.js?v=dual-seat-109",
"/gesture-control.js?v=android-pad-9",
"/pwa.js?v=android-pad-3",
"/console-v2.css?v=delivery-state-43",
"/console-v2.js?v=delivery-state-49",
"/console-v2.js?v=delivery-state-50",
"/self-extension.css?v=gscu-tcp-39",
"/self-extension.js?v=gscu-tcp-40",
"/assets/pets/self-extension-cat.png?v=3",