完善模式卡片、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
+214 -101
View File
@@ -4949,9 +4949,6 @@ function modeEditorFlowFromResult(result) {
const functionConfigs = result?.function_configs && typeof result.function_configs === "object" const functionConfigs = result?.function_configs && typeof result.function_configs === "object"
? result.function_configs ? result.function_configs
: {}; : {};
const executionSteps = Array.isArray(result?.execution_report?.spec?.results)
? result.execution_report.spec.results
: [];
return { return {
schema: "cognitive-agent-mode-command-flow/v1", schema: "cognitive-agent-mode-command-flow/v1",
source_request_id: result?.request_id || "", source_request_id: result?.request_id || "",
@@ -4959,7 +4956,7 @@ function modeEditorFlowFromResult(result) {
control_transport: result?.control_transport || "", control_transport: result?.control_transport || "",
function_configs: functionConfigs, function_configs: functionConfigs,
plans, plans,
execution_steps: executionSteps, execution_steps: [],
}; };
} }
@@ -5283,7 +5280,7 @@ function renderModeEditorFlow() {
const empty = document.createElement("p"); const empty = document.createElement("p");
empty.textContent = commandCount > 0 empty.textContent = commandCount > 0
? "已生成 CAN 控制流,但当前指令没有可读映射。" ? "已生成 CAN 控制流,但当前指令没有可读映射。"
: "点击“执行”后,这里会显示由后端生成的 CAN 控制指令流。"; : "点击“生成指令”后,这里会显示由后端解析、校验并编译的控制指令流。";
preview.append(empty); preview.append(empty);
} }
} }
@@ -5299,9 +5296,9 @@ function setModeEditorFlowExpanded(expanded) {
button.setAttribute("aria-expanded", String(nextExpanded)); button.setAttribute("aria-expanded", String(nextExpanded));
button.setAttribute( button.setAttribute(
"aria-label", "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>`; button.innerHTML = `<i data-lucide="${nextExpanded ? "minimize-2" : "maximize-2"}"></i>`;
refreshIcons(); refreshIcons();
} }
@@ -5328,8 +5325,8 @@ function openModeEditor(modeProfileId) {
$("#modeEditorName").value = mode.name || ""; $("#modeEditorName").value = mode.name || "";
$("#modeEditorInstruction").value = instruction; $("#modeEditorInstruction").value = instruction;
$("#modeEditorStatus").textContent = flow $("#modeEditorStatus").textContent = flow
? "已载入永久保存的自然语言和 CAN 指令流,可修改后重新执行。" ? "已载入永久保存的自然语言和控制指令流,可修改后重新生成。"
: "修改自然语言后需要重新执行,才能点击完成永久保存。"; : "修改自然语言后需要重新生成指令,才能点击完成永久保存。";
setModeEditorFlowExpanded(false); setModeEditorFlowExpanded(false);
backdrop.hidden = false; backdrop.hidden = false;
document.body.classList.add("mode-editor-open"); document.body.classList.add("mode-editor-open");
@@ -5360,8 +5357,8 @@ function invalidateModeEditorFlow() {
const instruction = $("#modeEditorInstruction").value.trim(); const instruction = $("#modeEditorInstruction").value.trim();
const current = instruction === state.modeEditor.executedInstruction; const current = instruction === state.modeEditor.executedInstruction;
$("#modeEditorStatus").textContent = current && modeEditorCommandCount(state.modeEditor.flow) > 0 $("#modeEditorStatus").textContent = current && modeEditorCommandCount(state.modeEditor.flow) > 0
? "当前自然语言已有 CAN 指令流,可点击完成永久保存。" ? "当前自然语言已有控制指令流,可点击完成永久保存。"
: "自然语言已修改,请重新点击执行生成 CAN 控制指令流。"; : "自然语言已修改,请重新点击“生成指令”。";
renderModeEditorFlow(); renderModeEditorFlow();
} }
@@ -5376,38 +5373,37 @@ async function executeModeEditorInstruction() {
const execute = $("#modeEditorExecute"); const execute = $("#modeEditorExecute");
execute.disabled = true; execute.disabled = true;
execute.classList.add("loading"); 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; $("#modeEditorComplete").disabled = true;
$("#modeEditorStatus").textContent = "正在解析自然语言并生成、执行 CAN 控制指令流…"; $("#modeEditorStatus").textContent = "正在复用主页解析流程生成控制指令;不会执行设备…";
refreshIcons(); refreshIcons();
try { try {
const execution = await requestCommandAndWait(instruction); const { payload: result } = await requestNaturalLanguageControl(instruction, {
const result = execution.result; executionMode: "dry_run",
clientType: "mode_editor_compile",
previewOnly: true,
});
const flow = modeEditorFlowFromResult(result); const flow = modeEditorFlowFromResult(result);
const commandCount = modeEditorCommandCount(flow); const commandCount = modeEditorCommandCount(flow);
if (!result || commandCount === 0) { if (!result || commandCount === 0) {
throw new Error(execution.error?.message || "未生成可保存的 CAN 控制指令流"); throw new Error("未生成可保存的控制指令流");
} }
editor.flow = flow; editor.flow = flow;
editor.sourceRequestId = result.request_id; editor.sourceRequestId = result.request_id;
editor.executedInstruction = instruction; editor.executedInstruction = instruction;
$("#modeEditorStatus").textContent = `已生成 ${commandCount} 条 CAN 控制指令;执行状态:${result.status}。`; $("#modeEditorStatus").textContent = `已生成 ${commandCount} 条控制指令;尚未执行设备。点击“完成”保存到模式卡片。`;
setModeExecutionState(
editor.modeId,
["succeeded", "partially_succeeded"].includes(result.status) ? "succeeded" : "failed",
);
renderModeEditorFlow(); renderModeEditorFlow();
} catch (error) { } catch (error) {
editor.flow = null; editor.flow = null;
editor.sourceRequestId = ""; editor.sourceRequestId = "";
editor.executedInstruction = ""; editor.executedInstruction = "";
$("#modeEditorStatus").textContent = error.message || "CAN 控制指令流生成失败。"; $("#modeEditorStatus").textContent = error.message || "控制指令流生成失败。";
renderModeEditorFlow(); renderModeEditorFlow();
showToast(error.message || "CAN 控制指令流生成失败"); showToast(error.message || "控制指令流生成失败");
} finally { } finally {
execute.disabled = false; execute.disabled = false;
execute.classList.remove("loading"); execute.classList.remove("loading");
execute.innerHTML = '<i data-lucide="play"></i><span>执行</span>'; execute.innerHTML = '<i data-lucide="wand-sparkles"></i><span>生成指令</span>';
refreshIcons(); refreshIcons();
} }
} }
@@ -5426,7 +5422,7 @@ async function completeModeEditor() {
|| editor.executedInstruction !== instruction || editor.executedInstruction !== instruction
|| modeEditorCommandCount(editor.flow) === 0 || modeEditorCommandCount(editor.flow) === 0
) { ) {
showToast("请先执行当前自然语言并生成 CAN 控制指令流"); showToast("请先根据当前自然语言生成控制指令流");
return; return;
} }
const complete = $("#modeEditorComplete"); const complete = $("#modeEditorComplete");
@@ -5588,7 +5584,7 @@ async function deleteUserModeProfile(modeProfileId) {
if (!mode || !isUserModeProfile(mode)) return; if (!mode || !isUserModeProfile(mode)) return;
const presetOverride = mode.preset_override === true; const presetOverride = mode.preset_override === true;
const confirmation = presetOverride const confirmation = presetOverride
? `恢复“${mode.name}”的初始预设配置?当前保存的名称、自然语言和 CAN 指令流将被删除。` ? `恢复“${mode.name}”的初始预设配置?当前保存的名称、自然语言和控制指令流将被删除。`
: `删除自定义功能模式“${mode.name}”?删除后无法恢复。`; : `删除自定义功能模式“${mode.name}”?删除后无法恢复。`;
if (!window.confirm(confirmation)) return; 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) { function applyModeCommand(command, modeProfileId = null) {
if (!command) return; if (!command) return;
if ($("#runButton").disabled) { if ($("#runButton").disabled) {
showToast("上一条指令仍在执行,请稍候"); showToast("上一条指令仍在执行,请稍候");
return; return;
} }
const mode = modeProfiles().find((item) => item.id === modeProfileId);
if (mode?.command_flow?.source_request_id) {
void executeSavedModeProfile(mode);
return;
}
closeModeGallery({ restoreFocus: false }); closeModeGallery({ restoreFocus: false });
state.pendingModeProfileId = modeProfileId; state.pendingModeProfileId = modeProfileId;
if (modeProfileId) setModeExecutionState(modeProfileId, "running"); if (modeProfileId) setModeExecutionState(modeProfileId, "running");
@@ -7094,7 +7161,8 @@ function renderExecution(result) {
.startsWith("nfc_peripheral"); .startsWith("nfc_peripheral");
summary.className = `execution-summary ${submittedUnverified || partiallySucceeded ? "partial" : reportSucceeded ? "succeeded" : "failed"}`; summary.className = `execution-summary ${submittedUnverified || partiallySucceeded ? "partial" : reportSucceeded ? "succeeded" : "failed"}`;
const presetSave = Boolean(result.preset_config); 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 simulatedHil = report.metadata?.labels?.seat_feedback === "simulated_hardware";
const physical = report.spec.mode === "hil" && !simulatedHil; const physical = report.spec.mode === "hil" && !simulatedHil;
const executedDomains = [...new Set(report.spec.results.map((item) => item.domain))]; const executedDomains = [...new Set(report.spec.results.map((item) => item.domain))];
@@ -7379,7 +7447,9 @@ function renderResult(result) {
? `${domains.length} 域场景部分完成` ? `${domains.length} 域场景部分完成`
: executionSucceeded : executionSucceeded
? result.mode_profile ? result.mode_profile
? "长期模式配置已保存" ? savedModeExecution
? `${result.mode_profile.name}执行完成`
: "长期模式配置已保存"
: result.preset_config : result.preset_config
? "座椅姿态预设已保存" ? "座椅姿态预设已保存"
: submittedUnverified : submittedUnverified
@@ -7481,6 +7551,115 @@ async function postControlPhase(path, body, executionMode, onEvent = null) {
return payload; 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 = "车辆设备" } = {}) { async function executeCanCommands(canCommands, { deviceName = "车辆设备" } = {}) {
const commands = Array.isArray(canCommands) const commands = Array.isArray(canCommands)
? canCommands.map((command) => String(command || "").trim()).filter(Boolean) ? canCommands.map((command) => String(command || "").trim()).filter(Boolean)
@@ -7568,26 +7747,6 @@ async function submitRequest(event) {
setRunStatus("processing", "PROCESSING"); setRunStatus("processing", "PROCESSING");
$("#resultTitle").textContent = "正在执行车舱编排"; $("#resultTitle").textContent = "正在执行车舱编排";
try { 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) => { const onControlPhaseEvent = (phaseEvent) => {
if (phaseEvent.type !== "validated_commands" || !phaseEvent.payload) return; if (phaseEvent.type !== "validated_commands" || !phaseEvent.payload) return;
window.dispatchEvent(new CustomEvent( window.dispatchEvent(new CustomEvent(
@@ -7595,67 +7754,21 @@ async function submitRequest(event) {
{ detail: phaseEvent.payload }, { detail: phaseEvent.payload },
)); ));
}; };
if (state.provider === "openai") { const { payload, supplementWarning } = await requestNaturalLanguageControl(
const preview = await postControlPhase( utterance,
"/api/requests/preview", {
requestBody,
state.executionMode,
onControlPhaseEvent, onControlPhaseEvent,
); onPreview: (preview) => {
if (!preview) return;
renderResult(preview); renderResult(preview);
completedResult = preview; completedResult = preview;
if (preview.supplement_required) { },
onSupplementStart: () => {
setRunStatus("processing", "CLOUD SUPPLEMENT"); setRunStatus("processing", "CLOUD SUPPLEMENT");
$("#resultTitle").textContent = "本地指令已处理,正在补充云端解析"; $("#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; 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;
}
renderResult(payload); renderResult(payload);
completedResult = payload; completedResult = payload;
const peripheralRoute = String(payload.orchestration_route || "") 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)}"> <li data-can-channel="${escapeHtml(item.channel)}" data-delivery-state="${escapeHtml(item.deliveryState)}">
<span> <span>
<b class="v2-can-command-channel">${escapeHtml(item.channel)}</b> <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> <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> <i class="v2-can-command-category">${escapeHtml(item.category)}</i>
</span> </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/fflate.min.js" defer></script>
<script src="/vendor/three/0.147.0/FBXLoader.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="/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 type="module" src="/gesture-control.js?v=android-pad-9"></script>
<script src="/pwa.js?v=android-pad-3" defer></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> <script src="/self-extension.js?v=gscu-tcp-40" defer></script>
</head> </head>
<body class="console-v2"> <body class="console-v2">
@@ -574,7 +574,7 @@
<div> <div>
<span>MODE WORKFLOW</span> <span>MODE WORKFLOW</span>
<h2 id="modeEditorTitle">编辑功能模式</h2> <h2 id="modeEditorTitle">编辑功能模式</h2>
<p id="modeEditorDescription">执行后生成 CAN 控制指令流;完成后永久保存到当前模式卡片。</p> <p id="modeEditorDescription">复用主页自然语言解析流程生成控制指令;此步骤不会执行设备。</p>
</div> </div>
<button id="modeEditorClose" type="button" aria-label="关闭模式编辑器"><i data-lucide="x"></i></button> <button id="modeEditorClose" type="button" aria-label="关闭模式编辑器"><i data-lucide="x"></i></button>
</header> </header>
@@ -591,22 +591,22 @@
</div> </div>
<section class="mode-editor-flow" aria-live="polite"> <section class="mode-editor-flow" aria-live="polite">
<header> <header>
<span><i data-lucide="workflow"></i>CAN 控制指令流</span> <span><i data-lucide="workflow"></i>控制指令流</span>
<div class="mode-editor-flow-tools"> <div class="mode-editor-flow-tools">
<b id="modeEditorFlowCount">尚未生成</b> <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> <i data-lucide="maximize-2"></i>
</button> </button>
</div> </div>
</header> </header>
<div class="mode-editor-flow-preview empty" id="modeEditorFlowPreview"> <div class="mode-editor-flow-preview empty" id="modeEditorFlowPreview">
<p>点击“执行”后,这里会显示由后端生成的 CAN 控制指令流。</p> <p>点击“生成指令”后,这里会显示由后端解析、校验并编译的控制指令流。</p>
</div> </div>
</section> </section>
<p class="mode-editor-status" id="modeEditorStatus">修改自然语言后需要重新执行,才能点击完成永久保存。</p> <p class="mode-editor-status" id="modeEditorStatus">修改自然语言后需要重新生成指令,才能点击完成永久保存。</p>
<footer class="mode-editor-actions"> <footer class="mode-editor-actions">
<button id="modeEditorCancel" type="button">取消</button> <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> <button class="mode-editor-complete" id="modeEditorComplete" type="button" disabled><i data-lucide="save"></i><span>完成</span></button>
</footer> </footer>
</section> </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 SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`; const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [ const APP_SHELL = [
@@ -6,11 +6,11 @@ const APP_SHELL = [
"/index.html", "/index.html",
"/styles.css?v=dual-seat-16", "/styles.css?v=dual-seat-16",
"/network.js?v=android-pad-2", "/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", "/gesture-control.js?v=android-pad-9",
"/pwa.js?v=android-pad-3", "/pwa.js?v=android-pad-3",
"/console-v2.css?v=delivery-state-43", "/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.css?v=gscu-tcp-39",
"/self-extension.js?v=gscu-tcp-40", "/self-extension.js?v=gscu-tcp-40",
"/assets/pets/self-extension-cat.png?v=3", "/assets/pets/self-extension-cat.png?v=3",
@@ -14,7 +14,13 @@
"parameters": {"type": "object"}, "parameters": {"type": "object"},
"parse_source": { "parse_source": {
"type": "string", "type": "string",
"enum": ["local_rule", "cloud_model", "cloud_supplement", "direct_control"] "enum": [
"local_rule",
"cloud_model",
"cloud_supplement",
"direct_control",
"saved_mode_command_flow"
]
}, },
"motion": { "motion": {
"type": "object", "type": "object",
@@ -112,10 +112,16 @@ class ModeOperation:
) )
def to_request(self, mode_name: str) -> dict[str, Any]: def to_request(self, mode_name: str) -> dict[str, Any]:
target_scope = self.target_scope
if self.domain == "seat" and target_scope == "front_right":
# Compiled seat operations use the physical zone name, while the
# model-plan adapter exposes the logical passenger target. Normalize
# persisted cards so a validated target is not queried again.
target_scope = "passenger"
return { return {
"domain": self.domain, "domain": self.domain,
"capability": self.capability, "capability": self.capability,
"target_scope": self.target_scope, "target_scope": target_scope,
"operation": self.operation, "operation": self.operation,
"parameters": [ "parameters": [
{"name": name, "value": value} {"name": name, "value": value}
@@ -450,6 +456,26 @@ class ModeProfileRegistry:
public.append(item) public.append(item)
return public return public
def get_profile(
self,
mode_id: str,
vehicle_id: str | None = None,
) -> ModeProfile:
"""Resolve the effective preset or user override by its stable ID."""
clean_mode_id = str(mode_id).strip()
profile = next(
(
item
for item in self._profiles_for_vehicle(vehicle_id)
if item.id == clean_mode_id
),
None,
)
if profile is None:
raise KeyError(clean_mode_id)
return profile
def match_text( def match_text(
self, self,
utterance: str, utterance: str,
+147 -42
View File
@@ -608,6 +608,59 @@ class SeatDemoService:
"vehicle_id": resolved_vehicle_id, "vehicle_id": resolved_vehicle_id,
} }
def get_mode_profile_card(
self,
mode_id: str,
vehicle_id: str | None = None,
) -> ModeProfile:
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
return self.mode_profiles.get_profile(mode_id, resolved_vehicle_id)
def execute_mode_profile_card(
self,
mode_id: str,
*,
conversation_id: str | None = None,
execution_mode: str = "dry_run",
approval_token: str | None = None,
vehicle_id: str | None = None,
simulation_fault: str = "normal",
hardware_client_id: str | None = None,
hardware_access_bypass: bool = True,
control_transport: str = WIRED_CAN_TRANSPORT,
) -> dict[str, Any]:
"""Execute a persisted mode by ID without interpreting natural language."""
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
profile = self.mode_profiles.get_profile(mode_id, resolved_vehicle_id)
if not profile.command_flow or not profile.command_flow.get("plans"):
raise ValueError("该模式尚未生成可直接执行的控制指令流。")
result = self.process(
profile.instruction or f"进入{profile.name}",
provider="local",
conversation_id=conversation_id,
execution_mode=execution_mode,
approval_token=approval_token,
vehicle_id=resolved_vehicle_id,
simulation_fault=simulation_fault,
hardware_client_id=hardware_client_id,
hardware_access_bypass=hardware_access_bypass,
control_transport=control_transport,
_direct_mode_profile_id=profile.id,
)
public_profile = profile.public_dict()
result["provider_used"] = "saved_mode_command_flow"
result["utterance"] = profile.name
result["matched_modes"] = [public_profile]
result["mode_profile"] = public_profile
result["orchestration_route"] = "saved_mode_command_flow"
result["decision_summary"] = [
f"直接载入已保存的模式“{profile.name}”,未再次解析自然语言。",
*list(result.get("decision_summary") or []),
]
self._persist(result)
return result
def save_mode_profile_card( def save_mode_profile_card(
self, self,
mode_id: str, mode_id: str,
@@ -616,7 +669,7 @@ class SeatDemoService:
source_request_id: str, source_request_id: str,
vehicle_id: str | None = None, vehicle_id: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Persist one edited mode from a server-generated CAN command flow.""" """Persist one edited mode from a server-generated control flow."""
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
clean_mode_id = str(mode_id).strip() clean_mode_id = str(mode_id).strip()
@@ -632,7 +685,7 @@ class SeatDemoService:
if len(clean_instruction) > 1000: if len(clean_instruction) > 1000:
raise ValueError("自然语言指令不能超过 1000 个字符。") raise ValueError("自然语言指令不能超过 1000 个字符。")
if not MODE_EDITOR_REQUEST_ID_PATTERN.fullmatch(clean_request_id): if not MODE_EDITOR_REQUEST_ID_PATTERN.fullmatch(clean_request_id):
raise ValueError("CAN 指令流来源请求编号无效。") raise ValueError("控制指令流来源请求编号无效。")
visible_profiles = self.mode_profiles._profiles_for_vehicle( # noqa: SLF001 visible_profiles = self.mode_profiles._profiles_for_vehicle( # noqa: SLF001
resolved_vehicle_id resolved_vehicle_id
@@ -653,15 +706,15 @@ class SeatDemoService:
result_path = self.artifact_root / clean_request_id / "result.json" result_path = self.artifact_root / clean_request_id / "result.json"
if not result_path.is_file(): if not result_path.is_file():
raise ValueError("未找到执行按钮生成的 CAN 指令流,请重新执行。") raise ValueError("未找到“生成指令”按钮产生的控制指令流,请重新生成。")
try: try:
source_result = json.loads(result_path.read_text(encoding="utf-8")) source_result = json.loads(result_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc: except (OSError, json.JSONDecodeError) as exc:
raise ValueError("CAN 指令流来源结果无法读取,请重新执行。") from exc raise ValueError("控制指令流来源结果无法读取,请重新生成。") from exc
if not isinstance(source_result, dict) or source_result.get( if not isinstance(source_result, dict) or source_result.get(
"request_id" "request_id"
) != clean_request_id: ) != clean_request_id:
raise ValueError("CAN 指令流来源结果不完整,请重新执行。") raise ValueError("控制指令流来源结果不完整,请重新生成。")
recorded_instruction = str(source_result.get("utterance", "")).strip() recorded_instruction = str(source_result.get("utterance", "")).strip()
if not recorded_instruction: if not recorded_instruction:
source_spans = { source_spans = {
@@ -676,12 +729,12 @@ class SeatDemoService:
if len(source_spans) == 1: if len(source_spans) == 1:
recorded_instruction = next(iter(source_spans)) recorded_instruction = next(iter(source_spans))
if recorded_instruction and recorded_instruction != clean_instruction: if recorded_instruction and recorded_instruction != clean_instruction:
raise ValueError("自然语言指令已修改,请重新点击执行生成 CAN 指令流。") raise ValueError("自然语言指令已修改,请重新点击“生成指令”。")
plans = source_result.get("command_plans") or {} plans = source_result.get("command_plans") or {}
configs = source_result.get("function_configs") or {} configs = source_result.get("function_configs") or {}
if not isinstance(plans, dict) or not isinstance(configs, dict): if not isinstance(plans, dict) or not isinstance(configs, dict):
raise ValueError("执行结果中没有可永久化的 CAN 指令流。") raise ValueError("解析结果中没有可永久化的控制指令流。")
executable_plan_keys = { executable_plan_keys = {
key key
for key, plan in plans.items() for key, plan in plans.items()
@@ -691,7 +744,7 @@ class SeatDemoService:
and plan["spec"]["commands"] and plan["spec"]["commands"]
} }
if not executable_plan_keys: if not executable_plan_keys:
raise ValueError("请先点击执行并成功生成 CAN 控制指令流。") raise ValueError("请先点击“生成指令”并成功生成控制指令流。")
operations: list[dict[str, Any]] = [] operations: list[dict[str, Any]] = []
signatures: set[tuple[str, str, str, str]] = set() signatures: set[tuple[str, str, str, str]] = set()
@@ -707,7 +760,12 @@ class SeatDemoService:
if not isinstance(operation, dict): if not isinstance(operation, dict):
continue continue
capability = str(operation.get("capability", "")).strip() capability = str(operation.get("capability", "")).strip()
target = str((operation.get("target") or {}).get("zone", "")).strip() operation_target = operation.get("target") or {}
target = str(
operation_target.get("instance")
or operation_target.get("zone")
or ""
).strip()
parameters = operation.get("parameters") or {} parameters = operation.get("parameters") or {}
if not domain or not capability or not target or not isinstance( if not domain or not capability or not target or not isinstance(
parameters, dict parameters, dict
@@ -733,37 +791,8 @@ class SeatDemoService:
} }
) )
if not operations: if not operations:
raise ValueError("CAN 指令流没有关联到可复用的模式操作。") raise ValueError("控制指令流没有关联到可复用的模式操作。")
execution_steps = []
for item in (
(source_result.get("execution_report") or {}).get("spec", {}).get(
"results", []
)
):
if not isinstance(item, dict):
continue
telemetry = item.get("telemetry") or {}
command_evidence = {
key: deepcopy(telemetry[key])
for key in (
"command_frame",
"sent_frames",
"mqtt_messages",
"response_frame",
"requested_parameters",
)
if key in telemetry
}
if command_evidence:
execution_steps.append(
{
"ref": item.get("ref"),
"domain": item.get("domain"),
"status": item.get("status"),
**command_evidence,
}
)
command_flow = { command_flow = {
"schema": "cognitive-agent-mode-command-flow/v1", "schema": "cognitive-agent-mode-command-flow/v1",
"source_request_id": clean_request_id, "source_request_id": clean_request_id,
@@ -776,7 +805,10 @@ class SeatDemoService:
if key in executable_plan_keys and key in configs if key in executable_plan_keys and key in configs
}, },
"plans": {key: deepcopy(plans[key]) for key in plans if key in executable_plan_keys}, "plans": {key: deepcopy(plans[key]) for key in plans if key in executable_plan_keys},
"execution_steps": execution_steps, # A mode card stores only the validated, deterministic plan. Live
# delivery evidence belongs to each later execution, not to the
# generation workflow.
"execution_steps": [],
} }
saved = self.mode_profiles.save_mode( saved = self.mode_profiles.save_mode(
clean_name, clean_name,
@@ -1677,6 +1709,7 @@ class SeatDemoService:
_allow_empty_control_result: bool = False, _allow_empty_control_result: bool = False,
_on_commands_validated: Callable[[dict[str, Any]], None] | None = None, _on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
_direct_can_batch: tuple[list[str], dict[str, Any]] | None = None, _direct_can_batch: tuple[list[str], dict[str, Any]] | None = None,
_direct_mode_profile_id: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
control_transport = self._validate_control_transport(control_transport) control_transport = self._validate_control_transport(control_transport)
# Creating a reusable scene is a configuration workflow. Synthesize # Creating a reusable scene is a configuration workflow. Synthesize
@@ -1702,6 +1735,7 @@ class SeatDemoService:
_allow_empty_control_result, _allow_empty_control_result,
_on_commands_validated, _on_commands_validated,
_direct_can_batch, _direct_can_batch,
_direct_mode_profile_id,
) )
if control_transport == WIRELESS_MQTT_TRANSPORT: if control_transport == WIRELESS_MQTT_TRANSPORT:
# The head unit owns both physical CAN channels in wireless mode. # The head unit owns both physical CAN channels in wireless mode.
@@ -1723,6 +1757,7 @@ class SeatDemoService:
_allow_empty_control_result, _allow_empty_control_result,
_on_commands_validated, _on_commands_validated,
_direct_can_batch, _direct_can_batch,
_direct_mode_profile_id,
) )
if hardware_access_bypass or self._driver_only_vehicle_execution_active( if hardware_access_bypass or self._driver_only_vehicle_execution_active(
control_transport control_transport
@@ -1744,6 +1779,7 @@ class SeatDemoService:
_allow_empty_control_result, _allow_empty_control_result,
_on_commands_validated, _on_commands_validated,
_direct_can_batch, _direct_can_batch,
_direct_mode_profile_id,
) )
authorized_client_id = self.hardware_access.ensure_authorized(hardware_client_id) authorized_client_id = self.hardware_access.ensure_authorized(hardware_client_id)
with self.hardware_execution.reserve(): with self.hardware_execution.reserve():
@@ -1764,6 +1800,7 @@ class SeatDemoService:
_allow_empty_control_result, _allow_empty_control_result,
_on_commands_validated, _on_commands_validated,
_direct_can_batch, _direct_can_batch,
_direct_mode_profile_id,
) )
def _process_once( def _process_once(
@@ -1783,6 +1820,7 @@ class SeatDemoService:
allow_empty_control_result: bool = False, allow_empty_control_result: bool = False,
on_commands_validated: Callable[[dict[str, Any]], None] | None = None, on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
direct_can_batch: tuple[list[str], dict[str, Any]] | None = None, direct_can_batch: tuple[list[str], dict[str, Any]] | None = None,
direct_mode_profile_id: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
control_transport = self._validate_control_transport(control_transport) control_transport = self._validate_control_transport(control_transport)
utterance = utterance.strip() utterance = utterance.strip()
@@ -1831,6 +1869,14 @@ class SeatDemoService:
if not CONVERSATION_ID_PATTERN.fullmatch(conversation_id): if not CONVERSATION_ID_PATTERN.fullmatch(conversation_id):
raise ValueError("conversation_id must start with conv- and contain only lowercase letters, numbers, or hyphens") raise ValueError("conversation_id must start with conv- and contain only lowercase letters, numbers, or hyphens")
vehicle_profile = self.vehicle_profiles.get(vehicle_id) vehicle_profile = self.vehicle_profiles.get(vehicle_id)
direct_mode_profile = (
self.mode_profiles.get_profile(
direct_mode_profile_id,
vehicle_profile.id,
)
if direct_mode_profile_id
else None
)
if mode_create_name is not None: if mode_create_name is not None:
return self._process_mode_profile_create( return self._process_mode_profile_create(
@@ -1852,7 +1898,9 @@ class SeatDemoService:
) )
context["mode_profiles"] = self.mode_profiles.public_modes(vehicle_profile.id) context["mode_profiles"] = self.mode_profiles.public_modes(vehicle_profile.id)
matched_modes = ( matched_modes = (
[] [direct_mode_profile]
if direct_mode_profile is not None
else []
if direct_can_batch is not None if direct_can_batch is not None
else self.mode_profiles.match_text( else self.mode_profiles.match_text(
utterance, utterance,
@@ -1941,7 +1989,56 @@ class SeatDemoService:
result, result,
traces, traces,
) )
if direct_can_batch is not None: if direct_mode_profile is not None:
mode_requests = self.mode_profiles.requests_for_profiles(
[direct_mode_profile]
)
detected_domains = list(
dict.fromkeys(
str(item.get("domain") or "")
for item in mode_requests
if str(item.get("domain") or "")
)
)
parsed = self.model_plan_adapter.adapt(
{
"detected_domains": detected_domains or ["seat"],
"requests": mode_requests,
"ambiguities": [],
"decision_summary": [
f"已载入模式“{direct_mode_profile.name}”保存的结构化操作。"
],
"confidence": 1.0,
},
context,
vehicle_profile,
)
saved_native_actions: list[dict[str, Any]] = []
for request in mode_requests:
parameters = {
str(item.get("name")): deepcopy(item.get("value"))
for item in request.get("parameters", [])
if isinstance(item, dict) and str(item.get("name") or "")
}
action = {
"domain": str(request.get("domain") or ""),
"capability": str(request.get("capability") or ""),
"target": str(request.get("target_scope") or "vehicle"),
"parameters": parameters,
"operation": str(request.get("operation") or "set"),
"reason": str(request.get("reason") or ""),
"execution_backend": "native_can",
"parse_source": "saved_mode_command_flow",
}
if self.native_can_runtime.supports(action):
saved_native_actions.append(action)
parsed["native_can_actions"] = [
*list(parsed.get("native_can_actions", [])),
*saved_native_actions,
]
provider_used = "saved_mode_command_flow"
provider_notice = None
elif direct_can_batch is not None:
parsed = deepcopy(direct_can_batch[1]) parsed = deepcopy(direct_can_batch[1])
provider_used = "direct_can_control" provider_used = "direct_can_control"
provider_notice = None provider_notice = None
@@ -1980,6 +2077,11 @@ class SeatDemoService:
relative_resolution.claimed_controls if relative_resolution else None relative_resolution.claimed_controls if relative_resolution else None
), ),
) )
if direct_mode_profile is not None:
parsed = self._mark_control_parse_source(
parsed,
"saved_mode_command_flow",
)
if local_preview: if local_preview:
parsed["ambiguities"] = [] parsed["ambiguities"] = []
parsed["unresolved_spans"] = [] parsed["unresolved_spans"] = []
@@ -1993,12 +2095,15 @@ class SeatDemoService:
parsed = self._partition_native_actions( parsed = self._partition_native_actions(
parsed, parsed,
promote_supported=( promote_supported=(
direct_mode_profile is not None
or (
hardware_request hardware_request
and ( and (
self.vehicle_mqtt_settings.enabled self.vehicle_mqtt_settings.enabled
if control_transport == WIRELESS_MQTT_TRANSPORT if control_transport == WIRELESS_MQTT_TRANSPORT
else self.vehicle_can_settings.enabled else self.vehicle_can_settings.enabled
) )
)
), ),
) )
result["parsed_control_signatures"] = [ result["parsed_control_signatures"] = [
@@ -35,6 +35,7 @@ MAX_PLAN_STEPS = 12
MIN_PLAN_DELAY_MS = 1 MIN_PLAN_DELAY_MS = 1
MAX_PLAN_DELAY_MS = 30_000 MAX_PLAN_DELAY_MS = 30_000
MAX_TOTAL_PLAN_DELAY_MS = 60_000 MAX_TOTAL_PLAN_DELAY_MS = 60_000
ADJACENT_INFRARED_COMMAND_DELAY_SECONDS = 0.8
DEFAULT_GSCU_MQTT_HOST = "127.0.0.1" DEFAULT_GSCU_MQTT_HOST = "127.0.0.1"
DEFAULT_GSCU_MQTT_PORT = 1883 DEFAULT_GSCU_MQTT_PORT = 1883
DEFAULT_GSCU_COMMAND_TOPIC = "topic/from_n7" DEFAULT_GSCU_COMMAND_TOPIC = "topic/from_n7"
@@ -2542,7 +2543,7 @@ class SelfExtensionService:
and isinstance(next_step, dict) and isinstance(next_step, dict)
and next_step.get("kind") == "peripheral" and next_step.get("kind") == "peripheral"
): ):
time.sleep(0.25) time.sleep(ADJACENT_INFRARED_COMMAND_DELAY_SECONDS)
succeeded = not interrupted and completed_steps == len(steps) succeeded = not interrupted and completed_steps == len(steps)
status = "succeeded" if succeeded else ( status = "succeeded" if succeeded else (
+220 -3
View File
@@ -795,15 +795,16 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
conversation_id: str | None, conversation_id: str | None,
execution_mode: str, execution_mode: str,
) -> dict[str, object]: ) -> dict[str, object]:
"""Execute already validated local-rule infrared capabilities.""" """Execute already validated infrared capabilities."""
capabilities = [ capabilities = [
{ {
**dict(entry["capability"]), **capability,
"parse_source": "local_rule", "parse_source": str(capability.get("parse_source") or "local_rule"),
} }
for entry in entries for entry in entries
if isinstance(entry, dict) and isinstance(entry.get("capability"), dict) if isinstance(entry, dict) and isinstance(entry.get("capability"), dict)
for capability in [dict(entry["capability"])]
] ]
if not capabilities: if not capabilities:
raise ExtensionIntegrationError("没有可执行的已学习红外能力。") raise ExtensionIntegrationError("没有可执行的已学习红外能力。")
@@ -1454,6 +1455,14 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
) )
def _handle_mode_profile_post(self, path: str) -> None: def _handle_mode_profile_post(self, path: str) -> None:
execute_match = re.fullmatch(
r"/api/mode-profiles/(?P<mode_id>[a-z0-9][a-z0-9_-]{0,63})/execute",
path,
)
if execute_match is not None:
self._handle_mode_profile_execute(execute_match.group("mode_id"))
return
mode_id = path.removeprefix("/api/mode-profiles/").strip() mode_id = path.removeprefix("/api/mode-profiles/").strip()
if not mode_id: if not mode_id:
self._json( self._json(
@@ -1482,6 +1491,214 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
HTTPStatus.BAD_REQUEST, HTTPStatus.BAD_REQUEST,
) )
def _saved_mode_infrared_entries(
self,
profile: object,
) -> list[dict[str, object]]:
command_flow = getattr(profile, "command_flow", None)
if not isinstance(command_flow, dict):
return []
configurations = command_flow.get("function_configs") or {}
extension_config = None
if isinstance(configurations, dict):
extension_config = configurations.get("self_extension")
if not isinstance(extension_config, dict):
extension_config = next(
(
config
for config in configurations.values()
if isinstance(config, dict)
and str((config.get("spec") or {}).get("domain"))
== "self_extension"
),
None,
)
operations = (
(extension_config.get("spec") or {}).get("operations") or []
if isinstance(extension_config, dict)
else []
)
if not operations:
return []
snapshot = self.nfc.snapshot()
card = snapshot.get("card") if isinstance(snapshot, dict) else None
active_uid = re.sub(
r"[^0-9A-Fa-f]",
"",
str((card or {}).get("uid") or ""),
).upper()
if not active_uid or str((card or {}).get("action") or "present") != "present":
raise ExtensionIntegrationError(
"该模式包含 NFC 红外指令,请先放置生成此模式时绑定的 NFC 卡。"
)
catalog = self.extension.list_peripheral_capabilities()
entries: list[dict[str, object]] = []
for index, operation in enumerate(operations):
if not isinstance(operation, dict) or operation.get("capability") != "infrared.emit":
continue
parameters = operation.get("parameters") or {}
target = operation.get("target") or {}
try:
slot = int(parameters.get("slot"))
except (TypeError, ValueError) as exc:
raise ExtensionIntegrationError(
"已保存模式包含无效的红外槽位。"
) from exc
device_id = str(target.get("device") or "").strip()
capability = next(
(
item
for item in catalog
if item.get("status") == "learned"
and item.get("enabled", True) is True
and int(item.get("slot") or 0) == slot
and str(item.get("device_id") or "") == device_id
and re.sub(
r"[^0-9A-Fa-f]",
"",
str(item.get("card_uid") or ""),
).upper()
== active_uid
),
None,
)
if capability is None:
raise ExtensionIntegrationError(
f"当前 NFC 未授权模式中的红外命令:{device_id} / IR {slot}。"
)
entries.append(
{
"index": index,
"clause": str(parameters.get("action") or f"IR {slot}"),
"capability": {
**capability,
"parse_source": "saved_mode_command_flow",
},
}
)
return entries
def _handle_mode_profile_execute(self, mode_id: str) -> None:
utterance = ""
source = "web_console_mode_card"
try:
payload = self._read_json_body(max_bytes=16 * 1024)
vehicle_id = str(payload.get("vehicle_id", "")) or None
profile = self.service.get_mode_profile_card(mode_id, vehicle_id)
utterance = profile.name
source = str(payload.get("client_type") or source)
execution_mode = str(payload.get("execution_mode", "dry_run"))
control_transport = str(
payload.get("control_transport", "wired_can")
)
conversation_id = (
str(payload["conversation_id"])
if payload.get("conversation_id")
else None
)
hardware_client_id = (
str(payload["hardware_client_id"])
if payload.get("hardware_client_id")
else None
)
self.vehicle_display_sync.update_task(
status="running",
utterance=utterance,
source=source,
)
# Validate the complete saved flow, including the live NFC
# binding, before any transport is allowed to execute.
entries = self._saved_mode_infrared_entries(profile)
can_result = self.service.execute_mode_profile_card(
mode_id,
conversation_id=conversation_id,
execution_mode=execution_mode,
approval_token=os.getenv(
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"
),
vehicle_id=vehicle_id,
simulation_fault=str(payload.get("simulation_fault", "normal")),
hardware_client_id=hardware_client_id,
hardware_access_bypass=self._is_unproxied_loopback_request(),
control_transport=control_transport,
)
if entries:
infrared_result = self._execute_local_peripheral_actions(
utterance,
entries,
provider="saved_mode_command_flow",
conversation_id=conversation_id,
execution_mode=execution_mode,
)
result = self._merge_can_and_peripheral_results(
utterance,
can_result,
infrared_result,
entries,
)
result["provider_used"] = "saved_mode_command_flow"
result["orchestration_route"] = "saved_mode_command_flow"
result["mode_profile"] = profile.public_dict()
self.service.persist_composed_result(result)
else:
result = can_result
self.vehicle_display_sync.update_task(
status=str(result.get("status") or "succeeded"),
utterance=utterance,
source=source,
result=result,
)
self._json(result, HTTPStatus.OK)
except KeyError:
self._json(
{"error": "not_found", "message": "未找到需要执行的模式卡片。"},
HTTPStatus.NOT_FOUND,
)
except HardwareAccessRequiredError as exc:
self._json(
{
"error": "hardware_access_required",
"message": str(exc),
"access_state": exc.state,
},
HTTPStatus.FORBIDDEN,
)
except HardwareBusyError as exc:
self._json(
{
"error": "hardware_busy",
"message": str(exc),
"retry_after_seconds": exc.retry_after_seconds,
},
HTTPStatus.CONFLICT,
)
except (HardwareSafetyError, CanGatewayError, GatewayError) as exc:
if utterance:
self.vehicle_display_sync.update_task(
status="failed",
utterance=utterance,
source=source,
error={"code": "mode_execution_failed", "message": str(exc)},
)
self._json(
{"error": "mode_execution_failed", "message": str(exc)},
HTTPStatus.SERVICE_UNAVAILABLE,
)
except (ExtensionIntegrationError, ValueError, json.JSONDecodeError) as exc:
if utterance:
self.vehicle_display_sync.update_task(
status="failed",
utterance=utterance,
source=source,
error={"code": "invalid_mode_command_flow", "message": str(exc)},
)
self._json(
{"error": "invalid_mode_command_flow", "message": str(exc)},
HTTPStatus.BAD_REQUEST,
)
def _handle_extension_post(self, path: str) -> None: def _handle_extension_post(self, path: str) -> None:
action_map = { action_map = {
"/api/extension/gscu/info": "info", "/api/extension/gscu/info": "info",
+250 -8
View File
@@ -289,6 +289,247 @@ operations:
self.service.mode_profiles = original_registry self.service.mode_profiles = original_registry
self.service.artifact_root = original_artifact_root self.service.artifact_root = original_artifact_root
def test_office_mode_generation_and_card_execution_reuse_saved_flow(self) -> None:
original_registry = self.service.mode_profiles
original_artifact_root = self.service.artifact_root
original_extension = SeatDemoHandler.extension
original_nfc = SeatDemoHandler.nfc
instruction = (
"空调温度设置为25度,风速设置为1档,"
"启动闪烁灯,颜色设置为红色"
)
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
preset_root = base / "presets"
user_root = base / "users"
preset = preset_root / "vehicles" / "denza_n7" / "office.yaml"
preset.parent.mkdir(parents=True)
preset.write_text(
"""
id: office
name: 办公模式
icon: briefcase
scope:
vehicle_ids: [denza_n7]
operations:
- domain: climate
capability: climate.set_temperature
target_scope: driver
parameters:
temperature_c: 24
""".strip()
+ "\n",
encoding="utf-8",
)
extension = SelfExtensionService(
SimulationGateway(),
catalog_db_path=base / "office-mode-ir.sqlite3",
)
extension.sync_peripheral_catalog(
[
{
"id": "ir-slot:tony-light-001:1",
"status": "learned",
"enabled": True,
"device_id": "tony-light-001",
"device_name": "闪灯玩具",
"object_names": ["闪灯玩具", "闪烁灯", "闪光灯"],
"card_uid": "2CC8B103",
"action": "打开",
"action_names": ["打开", "开启", "启动"],
"risk": "low",
"slot": 1,
},
{
"id": "ir-slot:tony-light-001:3",
"status": "learned",
"enabled": True,
"device_id": "tony-light-001",
"device_name": "闪灯玩具",
"object_names": ["闪灯玩具", "闪烁灯", "闪光灯"],
"card_uid": "2CC8B103",
"action": "切换到红色",
"action_names": [
"切换到红色",
"切换为红色",
"设置为红色",
],
"risk": "low",
"slot": 3,
},
]
)
self.service.mode_profiles = ModeProfileRegistry(
preset_root,
user_root=user_root,
)
self.service.artifact_root = base / "runs"
SeatDemoHandler.extension = extension
SeatDemoHandler.nfc = SimpleNamespace(
snapshot=lambda: {
"revision": 1,
"reader": {"online": True},
"card": {"action": "present", "uid": "2CC8B103"},
}
)
try:
generate_request = urllib.request.Request(
f"{self.base_url}/api/requests/preview",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"utterance": instruction,
"provider": "local",
"conversation_id": "conv-office-mode-generate",
"vehicle_id": "denza_n7",
"execution_mode": "dry_run",
"control_transport": "wired_can",
"client_type": "mode_editor_compile",
},
ensure_ascii=False,
).encode("utf-8"),
)
with self.opener.open(generate_request, timeout=3) as response:
generated = json.loads(response.read().decode("utf-8"))
save_request = urllib.request.Request(
f"{self.base_url}/api/mode-profiles/office",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"name": "办公模式",
"instruction": instruction,
"source_request_id": generated["request_id"],
"vehicle_id": "denza_n7",
},
ensure_ascii=False,
).encode("utf-8"),
)
with self.opener.open(save_request, timeout=3) as response:
saved = json.loads(response.read().decode("utf-8"))
execute_request = urllib.request.Request(
f"{self.base_url}/api/mode-profiles/office/execute",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"conversation_id": "conv-office-mode-execute",
"vehicle_id": "denza_n7",
"execution_mode": "dry_run",
"control_transport": "wired_can",
"client_type": "web_console_mode_card",
},
ensure_ascii=False,
).encode("utf-8"),
)
with patch.object(
self.service,
"_interpret",
side_effect=AssertionError(
"saved mode card must not reinterpret natural language"
),
), self.opener.open(execute_request, timeout=3) as response:
executed = json.loads(response.read().decode("utf-8"))
finally:
self.service.mode_profiles = original_registry
self.service.artifact_root = original_artifact_root
SeatDemoHandler.extension = original_extension
SeatDemoHandler.nfc = original_nfc
generated_commands = [
(
command.get("signals", {}).get("debug_command")
or (
f"infrared emit {command.get('signals', {}).get('slot')}"
if command.get("message") == "infrared.emit"
else None
)
)
for plan in generated.get("command_plans", {}).values()
for command in plan.get("spec", {}).get("commands", [])
]
self.assertEqual(
generated_commands,
[
"ac temp driver 25",
"ac fan 1",
"infrared emit 1",
"infrared emit 3",
],
)
self.assertEqual(saved["mode_profile"]["command_flow"]["execution_steps"], [])
self.assertEqual(executed["status"], "succeeded")
self.assertEqual(executed["utterance"], "办公模式")
self.assertEqual(executed["provider_used"], "saved_mode_command_flow")
self.assertEqual(executed["orchestration_route"], "saved_mode_command_flow")
self.assertEqual(
[item["slot"] for item in executed["infrared_actions"]],
[1, 3],
)
self.assertTrue(
all(
item["parse_source"] == "saved_mode_command_flow"
for item in executed["infrared_actions"]
)
)
def test_saved_mode_accepts_prefixed_self_extension_flow_key(self) -> None:
with tempfile.TemporaryDirectory() as directory:
extension = SelfExtensionService(
SimulationGateway(),
catalog_db_path=Path(directory, "prefixed-mode-ir.sqlite3"),
)
extension.sync_peripheral_catalog(
[
{
"id": "ir-slot:tony-light-001:1",
"status": "learned",
"enabled": True,
"device_id": "tony-light-001",
"device_name": "闪灯玩具",
"card_uid": "2CC8B103",
"action": "打开",
"slot": 1,
}
]
)
handler = object.__new__(SeatDemoHandler)
handler.extension = extension
handler.nfc = SimpleNamespace(
snapshot=lambda: {
"card": {"action": "present", "uid": "2CC8B103"}
}
)
profile = SimpleNamespace(
command_flow={
"function_configs": {
"local_self_extension": {
"spec": {
"domain": "self_extension",
"operations": [
{
"capability": "infrared.emit",
"target": {"device": "tony-light-001"},
"parameters": {"action": "打开", "slot": 1},
}
],
}
}
}
}
)
entries = handler._saved_mode_infrared_entries(profile)
self.assertEqual([item["capability"]["slot"] for item in entries], [1])
self.assertEqual(
entries[0]["capability"]["parse_source"],
"saved_mode_command_flow",
)
def test_head_unit_display_sync_tracks_tasks_and_usb_catalog(self) -> None: def test_head_unit_display_sync_tracks_tasks_and_usb_catalog(self) -> None:
task_request = urllib.request.Request( task_request = urllib.request.Request(
f"{self.base_url}/api/requests", f"{self.base_url}/api/requests",
@@ -344,14 +585,14 @@ operations:
def test_static_console_and_runtime_endpoint(self) -> None: def test_static_console_and_runtime_endpoint(self) -> None:
with self.opener.open(f"{self.base_url}/", timeout=3) as response: with self.opener.open(f"{self.base_url}/", timeout=3) as response:
html = response.read().decode("utf-8") html = response.read().decode("utf-8")
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-107", timeout=3) as response: with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-109", timeout=3) as response:
app_script = response.read().decode("utf-8") app_script = response.read().decode("utf-8")
with self.opener.open( with self.opener.open(
f"{self.base_url}/gesture-control.js?v=android-pad-9", timeout=3 f"{self.base_url}/gesture-control.js?v=android-pad-9", timeout=3
) as response: ) as response:
gesture_script = response.read().decode("utf-8") gesture_script = response.read().decode("utf-8")
with self.opener.open( with self.opener.open(
f"{self.base_url}/console-v2.js?v=delivery-state-49", timeout=3 f"{self.base_url}/console-v2.js?v=delivery-state-50", timeout=3
) as response: ) as response:
console_script = response.read().decode("utf-8") console_script = response.read().decode("utf-8")
with self.opener.open( with self.opener.open(
@@ -394,11 +635,11 @@ operations:
self.assertIn("CMD 0x431 · FB 0x41A · CH1", html) self.assertIn("CMD 0x431 · FB 0x41A · CH1", html)
self.assertIn("CMD 0x431 · FB 0x388 · CH1", html) self.assertIn("CMD 0x431 · FB 0x388 · CH1", html)
self.assertNotIn("透明车体 · 实体设备", html) self.assertNotIn("透明车体 · 实体设备", html)
self.assertIn('src="/app.js?v=dual-seat-107"', html) self.assertIn('src="/app.js?v=dual-seat-109"', html)
self.assertIn('href="/styles.css?v=dual-seat-16"', html) self.assertIn('href="/styles.css?v=dual-seat-16"', html)
self.assertIn('src="/gesture-control.js?v=android-pad-9"', html) self.assertIn('src="/gesture-control.js?v=android-pad-9"', html)
self.assertNotIn('class="vehicle-system-hud"', html) self.assertNotIn('class="vehicle-system-hud"', html)
self.assertIn('src="/console-v2.js?v=delivery-state-49"', html) self.assertIn('src="/console-v2.js?v=delivery-state-50"', html)
self.assertIn('href="/console-v2.css?v=delivery-state-43"', html) self.assertIn('href="/console-v2.css?v=delivery-state-43"', html)
self.assertIn('<body class="console-v2">', html) self.assertIn('<body class="console-v2">', html)
self.assertIn('id="v2BootScreen"', html) self.assertIn('id="v2BootScreen"', html)
@@ -641,7 +882,8 @@ operations:
) )
self.assertIn("const stageHasLayout = () =>", app_script) self.assertIn("const stageHasLayout = () =>", app_script)
self.assertIn("window.requestAnimationFrame(initSeatTwin)", app_script) self.assertIn("window.requestAnimationFrame(initSeatTwin)", app_script)
self.assertIn('client_type: "web_console"', app_script) self.assertIn('clientType = "web_console"', app_script)
self.assertIn("client_type: clientType", app_script)
self.assertIn("function executionVerificationStatus(result = {})", app_script) self.assertIn("function executionVerificationStatus(result = {})", app_script)
self.assertIn('"submitted_unverified"', app_script) self.assertIn('"submitted_unverified"', app_script)
self.assertIn("指令已提交,等待状态确认", app_script) self.assertIn("指令已提交,等待状态确认", app_script)
@@ -649,8 +891,8 @@ operations:
self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script) self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script)
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script) self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
self.assertIn('"cognitive-agent:module-change"', app_script) self.assertIn('"cognitive-agent:module-change"', app_script)
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v218"', service_worker) self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v220"', service_worker)
self.assertIn('"/app.js?v=dual-seat-107"', service_worker) self.assertIn('"/app.js?v=dual-seat-109"', service_worker)
self.assertIn('"/assets/pets/self-extension-cat.png?v=3"', service_worker) self.assertIn('"/assets/pets/self-extension-cat.png?v=3"', service_worker)
self.assertIn(".then(() => self.skipWaiting())", service_worker) self.assertIn(".then(() => self.skipWaiting())", service_worker)
self.assertNotIn("const cached = await caches.match(request)", service_worker) self.assertNotIn("const cached = await caches.match(request)", service_worker)
@@ -890,7 +1132,7 @@ operations:
) as response: ) as response:
extension_script = response.read().decode("utf-8") extension_script = response.read().decode("utf-8")
with self.opener.open( with self.opener.open(
f"{self.base_url}/app.js?v=dual-seat-107", timeout=3 f"{self.base_url}/app.js?v=dual-seat-109", timeout=3
) as response: ) as response:
app_script = response.read().decode("utf-8") app_script = response.read().decode("utf-8")
+176 -1
View File
@@ -621,7 +621,7 @@ operations:
if item["id"] == "office" if item["id"] == "office"
) )
self.assertEqual(persisted["name"], "专注办公模式") self.assertEqual(persisted["name"], "专注办公模式")
with self.assertRaisesRegex(ValueError, "重新点击执行"): with self.assertRaisesRegex(ValueError, "生成指令"):
self.service.save_mode_profile_card( self.service.save_mode_profile_card(
"office", "office",
"专注办公模式", "专注办公模式",
@@ -630,6 +630,181 @@ operations:
"denza_n7", "denza_n7",
) )
def test_saved_mode_card_executes_without_reinterpreting_instruction(self) -> None:
mode_root = Path(self.temporary_directory.name) / "direct-mode-presets"
user_root = Path(self.temporary_directory.name) / "direct-mode-user"
preset = mode_root / "vehicles" / "denza_n7" / "office.yaml"
preset.parent.mkdir(parents=True)
preset.write_text(
"""
id: office
name: 办公模式
icon: briefcase
scope:
vehicle_ids: [denza_n7]
operations:
- domain: climate
capability: climate.set_temperature
target_scope: driver
parameters:
temperature_c: 24
""".strip()
+ "\n",
encoding="utf-8",
)
self.service.mode_profiles = ModeProfileRegistry(
mode_root, user_root=user_root
)
instruction = "空调温度设置为25度,风速设置为1档"
generated = self.service.preview_native_can(
instruction,
provider="local",
conversation_id="conv-generate-office-card",
execution_mode="dry_run",
vehicle_id="denza_n7",
)
saved = self.service.save_mode_profile_card(
"office",
"办公模式",
instruction,
generated["request_id"],
"denza_n7",
)
with patch.object(
self.service,
"_interpret",
side_effect=AssertionError("saved mode must not reinterpret natural language"),
):
executed = self.service.execute_mode_profile_card(
"office",
conversation_id="conv-execute-office-card",
execution_mode="dry_run",
vehicle_id="denza_n7",
)
operations = [
operation
for config in executed.get("function_configs", {}).values()
for operation in config.get("spec", {}).get("operations", [])
]
self.assertEqual(executed["status"], "succeeded")
self.assertEqual(executed["utterance"], "办公模式")
self.assertEqual(executed["provider_used"], "saved_mode_command_flow")
self.assertEqual(executed["orchestration_route"], "saved_mode_command_flow")
self.assertEqual(
[
(
operation["capability"],
operation["target"]["zone"],
operation["parameters"],
operation["parse_source"],
)
for operation in operations
],
[
(
"climate.set_temperature",
"driver",
{"temperature_c": 25},
"saved_mode_command_flow",
),
(
"climate.set_fan_level",
"vehicle",
{"level": 1},
"saved_mode_command_flow",
),
],
)
self.assertEqual(
saved["mode_profile"]["command_flow"]["execution_steps"], []
)
def test_saved_mode_card_replays_front_right_dynamic_target_as_passenger(
self,
) -> None:
mode_root = Path(self.temporary_directory.name) / "dynamic-mode-presets"
user_root = Path(self.temporary_directory.name) / "dynamic-mode-user"
self.service.mode_profiles = ModeProfileRegistry(
mode_root,
user_root=user_root,
)
self.service.mode_profiles.save_mode(
"小憩模式",
[
{
"domain": "seat",
"capability": "seat.start_dynamic_mode",
"target_scope": "front_right",
"parameters": {"mode_id": 2},
},
{
"domain": "climate",
"capability": "climate.set_power",
"target_scope": "vehicle",
"parameters": {"state": "on"},
},
{
"domain": "roof",
"capability": "roof.set_shade_motion",
"target_scope": "vehicle",
"parameters": {"motion": "open"},
},
],
mode_id="nap",
vehicle_id="denza_n7",
instruction="打开摇篮模式,开启空调",
command_flow={
"schema": "cognitive-agent-mode-command-flow/v1",
"source_request_id": "req-saved-dynamic-mode",
"plans": {
"local_seat": {
"spec": {
"commands": [
{
"message": "Seat_Dynamic_Mode_Virtual",
"signals": {"Dynamic_Mode_ID": 2},
}
]
}
}
},
},
)
with patch.object(
self.service,
"_interpret",
side_effect=AssertionError("saved mode must not reinterpret natural language"),
):
executed = self.service.execute_mode_profile_card(
"nap",
conversation_id="conv-execute-saved-dynamic-card",
execution_mode="dry_run",
vehicle_id="denza_n7",
)
self.assertEqual(executed["status"], "succeeded")
self.assertEqual(executed["utterance"], "小憩模式")
self.assertEqual(executed["provider_used"], "saved_mode_command_flow")
self.assertFalse(
[item for item in executed.get("validation", []) if item["severity"] == "error"]
)
dynamic_request = next(
item
for item in executed["model_requests"]
if item["capability"] == "seat.start_dynamic_mode"
)
self.assertEqual(dynamic_request["target_scope"], "passenger")
self.assertTrue(executed["command_plans"])
replayed_capabilities = {
operation["capability"]
for config in executed["function_configs"].values()
for operation in config["spec"]["operations"]
}
self.assertIn("roof.set_shade_motion", replayed_capabilities)
def test_build_named_mode_executes_and_persists_in_one_turn(self) -> None: def test_build_named_mode_executes_and_persists_in_one_turn(self) -> None:
mode_root = Path(self.temporary_directory.name) / "modes" mode_root = Path(self.temporary_directory.name) / "modes"
self.service.mode_profiles = ModeProfileRegistry(mode_root) self.service.mode_profiles = ModeProfileRegistry(mode_root)
+1 -1
View File
@@ -139,4 +139,4 @@ def test_main_parsed_control_panel_renders_validated_infrared_operations() -> No
assert 'frameLabel: "GSCU1B TCP"' in console assert 'frameLabel: "GSCU1B TCP"' in console
assert "已解析控制指令" in console assert "已解析控制指令" in console
assert 'li[data-can-channel="IR"]' in styles assert 'li[data-can-channel="IR"]' in styles
assert "console-v2.js?v=delivery-state-49" in html assert "console-v2.js?v=delivery-state-50" in html
@@ -437,7 +437,19 @@ def test_mode_editor_renders_readable_commands_with_expandable_mqtt_sequences()
assert 'source: "published"' in app assert 'source: "published"' in app
assert 'source: flow?.control_transport === "wireless_mqtt" ? "projected" : "can"' in app assert 'source: flow?.control_transport === "wireless_mqtt" ? "projected" : "can"' in app
assert "function_configs: functionConfigs" in app assert "function_configs: functionConfigs" in app
assert "execution_steps: executionSteps" in app assert "execution_steps: []" in app
assert "requestNaturalLanguageControl(instruction" in app
assert 'executionMode: "dry_run"' in app
assert 'clientType: "mode_editor_compile"' in app
assert "previewOnly: true" in app
assert "function executeSavedModeProfile(mode)" in app
assert "/api/mode-profiles/${encodeURIComponent(mode.id)}/execute" in app
assert '$("#utterance").value = mode.name || mode.id;' in app
assert 'const savedModeExecution = result.orchestration_route === "saved_mode_command_flow";' in app
assert "`${result.mode_profile.name}执行完成`" in app
assert "生成指令" in html
assert ">执行</span></button>" not in html
assert ".mode-editor-command-card" in styles assert ".mode-editor-command-card" in styles
assert ".mode-editor-command-meta" in styles assert ".mode-editor-command-meta" in styles
+10 -7
View File
@@ -970,7 +970,7 @@ class SelfExtensionTests(unittest.TestCase):
["failed"], ["failed"],
) )
def test_submitted_emit_without_ack_continues_complete_infrared_plan(self) -> None: def test_power_on_then_white_waits_800ms_and_continues_without_ack(self) -> None:
service = SelfExtensionService(SimulationGateway()) service = SelfExtensionService(SimulationGateway())
catalog = [ catalog = [
{ {
@@ -984,7 +984,7 @@ class SelfExtensionTests(unittest.TestCase):
} }
for capability_id, action, slot in ( for capability_id, action, slot in (
("light-on", "打开", 1), ("light-on", "打开", 1),
("light-green", "切换到绿色", 4), ("light-white", "切换到白色", 6),
) )
] ]
@@ -1009,16 +1009,18 @@ class SelfExtensionTests(unittest.TestCase):
"execute_gateway", "execute_gateway",
side_effect=submit_without_ack, side_effect=submit_without_ack,
) as execute_gateway, ) as execute_gateway,
patch("cognitive_agent.application.self_extension.time.sleep"), patch(
"cognitive_agent.application.self_extension.time.sleep"
) as sleep,
): ):
result = service.execute_peripheral_plan_request( result = service.execute_peripheral_plan_request(
"开启闪光灯,颜色设置为绿色", "启动闪烁灯,颜色设置为白色",
{ {
"provider_used": "infrared_local_rules", "provider_used": "infrared_local_rules",
"summary": "打开闪光灯并切换为绿色", "summary": "启动闪烁灯并切换为白色",
"steps": [ "steps": [
{"kind": "peripheral", "capability_id": "light-on"}, {"kind": "peripheral", "capability_id": "light-on"},
{"kind": "peripheral", "capability_id": "light-green"}, {"kind": "peripheral", "capability_id": "light-white"},
], ],
}, },
catalog, catalog,
@@ -1027,11 +1029,12 @@ class SelfExtensionTests(unittest.TestCase):
) )
self.assertEqual(execute_gateway.call_count, 2) self.assertEqual(execute_gateway.call_count, 2)
sleep.assert_called_once_with(0.8)
self.assertEqual(result["status"], "succeeded") self.assertEqual(result["status"], "succeeded")
self.assertEqual(result["sequence"]["emit_count"], 2) self.assertEqual(result["sequence"]["emit_count"], 2)
self.assertEqual( self.assertEqual(
[item["telemetry"]["slot"] for item in result["operation_results"]], [item["telemetry"]["slot"] for item in result["operation_results"]],
[1, 4], [1, 6],
) )
self.assertEqual( self.assertEqual(
[ [
+1
View File
@@ -187,6 +187,7 @@ def test_frontend_two_stage_contract_and_source_badges() -> None:
assert "本地规则命令已执行;云端补充失败" in app assert "本地规则命令已执行;云端补充失败" in app
assert 'class="v2-can-command-source"' in console assert 'class="v2-can-command-source"' in console
assert 'item.parseSource === "direct_control" ? "设备控件"' in console assert 'item.parseSource === "direct_control" ? "设备控件"' in console
assert 'item.parseSource === "saved_mode_command_flow" ? "模式卡片"' in console
assert 'item.parseSource === "local_rule" ? "本地规则" : "云端模型"' in console assert 'item.parseSource === "local_rule" ? "本地规则" : "云端模型"' in console
assert 'item.deliveryLabel' in console assert 'item.deliveryLabel' in console
assert 'data-delivery-state="${escapeHtml(item.deliveryState)}"' in console assert 'data-delivery-state="${escapeHtml(item.deliveryState)}"' in console