From cbf230182d1d04683f72300da063901325e361d4 Mon Sep 17 00:00:00 2001 From: minghanchen <17188325+minghanchen@user.noreply.gitee.com> Date: Tue, 1 Sep 2026 16:18:57 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=A8=A1=E5=BC=8F=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E3=80=81NFC=E5=8F=8A=E5=A4=9A=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-console/app.js | 329 ++++++++++++------ apps/web-console/console-v2.js | 2 +- apps/web-console/index.html | 16 +- apps/web-console/service-worker.js | 6 +- contracts/schemas/function-config.schema.json | 8 +- .../application/mode_profile.py | 28 +- .../application/seat_service.py | 199 ++++++++--- .../application/self_extension.py | 3 +- src/cognitive_agent/server.py | 223 +++++++++++- tests/e2e/test_seat_web_api.py | 258 +++++++++++++- tests/integration/test_multidomain_service.py | 177 +++++++++- tests/unit/test_nfc_frontend_contract.py | 2 +- .../test_requested_frontend_enhancements.py | 14 +- tests/unit/test_self_extension.py | 17 +- tests/unit/test_two_stage_can_parsing.py | 1 + 15 files changed, 1092 insertions(+), 191 deletions(-) diff --git a/apps/web-console/app.js b/apps/web-console/app.js index ad43a24..500101b 100755 --- a/apps/web-console/app.js +++ b/apps/web-console/app.js @@ -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 = ``; 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 = '执行中…'; + execute.innerHTML = '生成中…'; $("#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 = '执行'; + execute.innerHTML = '生成指令'; 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 || "") diff --git a/apps/web-console/console-v2.js b/apps/web-console/console-v2.js index 7be340f..b2aab48 100644 --- a/apps/web-console/console-v2.js +++ b/apps/web-console/console-v2.js @@ -429,7 +429,7 @@
执行后生成 CAN 控制指令流;完成后永久保存到当前模式卡片。
+复用主页自然语言解析流程生成控制指令;此步骤不会执行设备。
点击“执行”后,这里会显示由后端生成的 CAN 控制指令流。
+点击“生成指令”后,这里会显示由后端解析、校验并编译的控制指令流。
修改自然语言后需要重新执行,才能点击完成永久保存。
+修改自然语言后需要重新生成指令,才能点击完成永久保存。
diff --git a/apps/web-console/service-worker.js b/apps/web-console/service-worker.js index feada6a..f4665e6 100644 --- a/apps/web-console/service-worker.js +++ b/apps/web-console/service-worker.js @@ -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", diff --git a/contracts/schemas/function-config.schema.json b/contracts/schemas/function-config.schema.json index 1774de8..c026946 100755 --- a/contracts/schemas/function-config.schema.json +++ b/contracts/schemas/function-config.schema.json @@ -14,7 +14,13 @@ "parameters": {"type": "object"}, "parse_source": { "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": { "type": "object", diff --git a/src/cognitive_agent/application/mode_profile.py b/src/cognitive_agent/application/mode_profile.py index e3b34a2..301adb2 100644 --- a/src/cognitive_agent/application/mode_profile.py +++ b/src/cognitive_agent/application/mode_profile.py @@ -112,10 +112,16 @@ class ModeOperation: ) 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 { "domain": self.domain, "capability": self.capability, - "target_scope": self.target_scope, + "target_scope": target_scope, "operation": self.operation, "parameters": [ {"name": name, "value": value} @@ -450,6 +456,26 @@ class ModeProfileRegistry: public.append(item) 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( self, utterance: str, diff --git a/src/cognitive_agent/application/seat_service.py b/src/cognitive_agent/application/seat_service.py index 9e2ec35..3693c35 100755 --- a/src/cognitive_agent/application/seat_service.py +++ b/src/cognitive_agent/application/seat_service.py @@ -608,6 +608,59 @@ class SeatDemoService: "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( self, mode_id: str, @@ -616,7 +669,7 @@ class SeatDemoService: source_request_id: str, vehicle_id: str | None = None, ) -> 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 clean_mode_id = str(mode_id).strip() @@ -632,7 +685,7 @@ class SeatDemoService: if len(clean_instruction) > 1000: raise ValueError("自然语言指令不能超过 1000 个字符。") 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 resolved_vehicle_id @@ -653,15 +706,15 @@ class SeatDemoService: result_path = self.artifact_root / clean_request_id / "result.json" if not result_path.is_file(): - raise ValueError("未找到执行按钮生成的 CAN 指令流,请重新执行。") + raise ValueError("未找到“生成指令”按钮产生的控制指令流,请重新生成。") try: source_result = json.loads(result_path.read_text(encoding="utf-8")) 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( "request_id" ) != clean_request_id: - raise ValueError("CAN 指令流来源结果不完整,请重新执行。") + raise ValueError("控制指令流来源结果不完整,请重新生成。") recorded_instruction = str(source_result.get("utterance", "")).strip() if not recorded_instruction: source_spans = { @@ -676,12 +729,12 @@ class SeatDemoService: if len(source_spans) == 1: recorded_instruction = next(iter(source_spans)) if recorded_instruction and recorded_instruction != clean_instruction: - raise ValueError("自然语言指令已修改,请重新点击执行生成 CAN 指令流。") + raise ValueError("自然语言指令已修改,请重新点击“生成指令”。") plans = source_result.get("command_plans") or {} configs = source_result.get("function_configs") or {} if not isinstance(plans, dict) or not isinstance(configs, dict): - raise ValueError("执行结果中没有可永久化的 CAN 指令流。") + raise ValueError("解析结果中没有可永久化的控制指令流。") executable_plan_keys = { key for key, plan in plans.items() @@ -691,7 +744,7 @@ class SeatDemoService: and plan["spec"]["commands"] } if not executable_plan_keys: - raise ValueError("请先点击执行并成功生成 CAN 控制指令流。") + raise ValueError("请先点击“生成指令”并成功生成控制指令流。") operations: list[dict[str, Any]] = [] signatures: set[tuple[str, str, str, str]] = set() @@ -707,7 +760,12 @@ class SeatDemoService: if not isinstance(operation, dict): continue 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 {} if not domain or not capability or not target or not isinstance( parameters, dict @@ -733,37 +791,8 @@ class SeatDemoService: } ) 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 = { "schema": "cognitive-agent-mode-command-flow/v1", "source_request_id": clean_request_id, @@ -776,7 +805,10 @@ class SeatDemoService: 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}, - "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( clean_name, @@ -1677,6 +1709,7 @@ class SeatDemoService: _allow_empty_control_result: bool = False, _on_commands_validated: Callable[[dict[str, Any]], None] | None = None, _direct_can_batch: tuple[list[str], dict[str, Any]] | None = None, + _direct_mode_profile_id: str | None = None, ) -> dict[str, Any]: control_transport = self._validate_control_transport(control_transport) # Creating a reusable scene is a configuration workflow. Synthesize @@ -1702,6 +1735,7 @@ class SeatDemoService: _allow_empty_control_result, _on_commands_validated, _direct_can_batch, + _direct_mode_profile_id, ) if control_transport == WIRELESS_MQTT_TRANSPORT: # The head unit owns both physical CAN channels in wireless mode. @@ -1723,6 +1757,7 @@ class SeatDemoService: _allow_empty_control_result, _on_commands_validated, _direct_can_batch, + _direct_mode_profile_id, ) if hardware_access_bypass or self._driver_only_vehicle_execution_active( control_transport @@ -1744,6 +1779,7 @@ class SeatDemoService: _allow_empty_control_result, _on_commands_validated, _direct_can_batch, + _direct_mode_profile_id, ) authorized_client_id = self.hardware_access.ensure_authorized(hardware_client_id) with self.hardware_execution.reserve(): @@ -1764,6 +1800,7 @@ class SeatDemoService: _allow_empty_control_result, _on_commands_validated, _direct_can_batch, + _direct_mode_profile_id, ) def _process_once( @@ -1783,6 +1820,7 @@ class SeatDemoService: allow_empty_control_result: bool = False, on_commands_validated: Callable[[dict[str, Any]], None] | None = None, direct_can_batch: tuple[list[str], dict[str, Any]] | None = None, + direct_mode_profile_id: str | None = None, ) -> dict[str, Any]: control_transport = self._validate_control_transport(control_transport) utterance = utterance.strip() @@ -1831,6 +1869,14 @@ class SeatDemoService: if not CONVERSATION_ID_PATTERN.fullmatch(conversation_id): raise ValueError("conversation_id must start with conv- and contain only lowercase letters, numbers, or hyphens") 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: return self._process_mode_profile_create( @@ -1852,7 +1898,9 @@ class SeatDemoService: ) context["mode_profiles"] = self.mode_profiles.public_modes(vehicle_profile.id) matched_modes = ( - [] + [direct_mode_profile] + if direct_mode_profile is not None + else [] if direct_can_batch is not None else self.mode_profiles.match_text( utterance, @@ -1941,7 +1989,56 @@ class SeatDemoService: result, 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]) provider_used = "direct_can_control" provider_notice = None @@ -1980,6 +2077,11 @@ class SeatDemoService: 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: parsed["ambiguities"] = [] parsed["unresolved_spans"] = [] @@ -1993,11 +2095,14 @@ class SeatDemoService: parsed = self._partition_native_actions( parsed, promote_supported=( - hardware_request - and ( - self.vehicle_mqtt_settings.enabled - if control_transport == WIRELESS_MQTT_TRANSPORT - else self.vehicle_can_settings.enabled + direct_mode_profile is not None + or ( + hardware_request + and ( + self.vehicle_mqtt_settings.enabled + if control_transport == WIRELESS_MQTT_TRANSPORT + else self.vehicle_can_settings.enabled + ) ) ), ) diff --git a/src/cognitive_agent/application/self_extension.py b/src/cognitive_agent/application/self_extension.py index 30dd3b6..cd87127 100644 --- a/src/cognitive_agent/application/self_extension.py +++ b/src/cognitive_agent/application/self_extension.py @@ -35,6 +35,7 @@ MAX_PLAN_STEPS = 12 MIN_PLAN_DELAY_MS = 1 MAX_PLAN_DELAY_MS = 30_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_PORT = 1883 DEFAULT_GSCU_COMMAND_TOPIC = "topic/from_n7" @@ -2542,7 +2543,7 @@ class SelfExtensionService: and isinstance(next_step, dict) 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) status = "succeeded" if succeeded else ( diff --git a/src/cognitive_agent/server.py b/src/cognitive_agent/server.py index b457cfb..3c6ae3a 100755 --- a/src/cognitive_agent/server.py +++ b/src/cognitive_agent/server.py @@ -795,15 +795,16 @@ class SeatDemoHandler(BaseHTTPRequestHandler): conversation_id: str | None, execution_mode: str, ) -> dict[str, object]: - """Execute already validated local-rule infrared capabilities.""" + """Execute already validated infrared capabilities.""" capabilities = [ { - **dict(entry["capability"]), - "parse_source": "local_rule", + **capability, + "parse_source": str(capability.get("parse_source") or "local_rule"), } for entry in entries if isinstance(entry, dict) and isinstance(entry.get("capability"), dict) + for capability in [dict(entry["capability"])] ] if not capabilities: raise ExtensionIntegrationError("没有可执行的已学习红外能力。") @@ -1454,6 +1455,14 @@ class SeatDemoHandler(BaseHTTPRequestHandler): ) def _handle_mode_profile_post(self, path: str) -> None: + execute_match = re.fullmatch( + r"/api/mode-profiles/(?P