"""Dependency-light HTTP server for the cabin orchestration demo.""" from __future__ import annotations import argparse import hmac import ipaddress import json import mimetypes import os import re import socket import sys import threading import time from collections.abc import Callable, Mapping from copy import deepcopy from http import HTTPStatus from http.cookies import CookieError, SimpleCookie from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from urllib.parse import ParseResult, parse_qs, quote, unquote, urlparse from cognitive_agent.config_loader import load_local_env REPOSITORY_ROOT = Path(__file__).resolve().parents[2] # Repository-local launch configuration is deliberate and must not be shadowed # by stale variables inherited from an IDE or an earlier model experiment. load_local_env(REPOSITORY_ROOT / ".env", override=True) load_local_env(REPOSITORY_ROOT / ".env.auth", override=True) from cognitive_agent.application.assistant_capabilities import ( # noqa: E402 AssistantCapabilityCatalog, ) from cognitive_agent.application.calibration.service import ( # noqa: E402 ApiError as CalibrationApiError, ) from cognitive_agent.application.calibration.service import ( # noqa: E402 CalibrationService, ) from cognitive_agent.application.codex_assistant import ( # noqa: E402 CodexAssistantError, CodexAssistantService, CodexVehicleToolError, ) from cognitive_agent.application.extension_control import ( # noqa: E402 ExtensionControlConsumer, ) from cognitive_agent.application.extension_toy_publisher import ( # noqa: E402 EXTENSION_TOY_BURST_COUNT, EXTENSION_TOY_BURST_WINDOW_S, NfcExtensionToyPublisher, ) from cognitive_agent.application.gesture_bridge import GestureCapabilityBridge # noqa: E402 from cognitive_agent.application.mode_profile_intent import ( # noqa: E402 normalize_mode_profile_name, parse_mode_profile_update, ) from cognitive_agent.application.mode_scene_catalog import ( # noqa: E402 MODE_SCENE_MQTT_BURST_COUNT, MODE_SCENE_MQTT_BURST_WINDOW_S, ) from cognitive_agent.application.pet_cards import PetCardService # noqa: E402 from cognitive_agent.application.mode_schedule_service import public_schedule_status # noqa: E402 from cognitive_agent.application.seat_service import ( # noqa: E402 LOCAL_CONTROL_BUDGET_MS, SeatDemoService, WIRELESS_MQTT_TRANSPORT, ) from cognitive_agent.application.self_extension import ( # noqa: E402 ExtensionIntegrationError, GatewayError, SelfExtensionService, active_peripheral_plan_catalog, partition_active_peripheral_intent, resolve_active_peripheral_command, ) from cognitive_agent.application.self_repair import SelfRepairService # noqa: E402 from cognitive_agent.auth import ( # noqa: E402 AuthConfig, AuthRuntime, is_json_content_type, origin_matches_host, verify_password, ) from cognitive_agent.infrastructure.can.access import ( # noqa: E402 HardwareAccessRequiredError, ) from cognitive_agent.infrastructure.can.gateway import CanGatewayError # noqa: E402 from cognitive_agent.infrastructure.can.hardware import ( # noqa: E402 HardwareBusyError, HardwareSafetyError, ) from cognitive_agent.infrastructure.can.mqtt_transport import ( # noqa: E402 MQTT_EXTENSION_CONTROL_TOPIC, MQTT_EXTENSION_TOY_TOPIC, MQTT_MODE_SCENE_TOPIC, ) from cognitive_agent.infrastructure.codex.app_server import ( # noqa: E402 CodexAppServerClient, CodexAppServerError, ) from cognitive_agent.infrastructure.llm.openai_responses import OpenAIIntentError # noqa: E402 from cognitive_agent.infrastructure.nfc import NfcReaderService # noqa: E402 from cognitive_agent.infrastructure.speech import ( # noqa: E402 SpeechRecognitionError, SpeechTranscriber, ) WEB_CONSOLE_SOURCE_ROOT = REPOSITORY_ROOT / "apps" / "web-console" WEB_CONSOLE_DIST_ROOT = WEB_CONSOLE_SOURCE_ROOT / "dist" STATIC_ROOT = ( WEB_CONSOLE_DIST_ROOT if (WEB_CONSOLE_DIST_ROOT / "index.html").is_file() else WEB_CONSOLE_SOURCE_ROOT ) DEFAULT_MODE_SCENE_CATALOG_PATH = ( REPOSITORY_ROOT.parent / "scenses" / "demo-scenes.json" ) mimetypes.add_type("application/manifest+json", ".webmanifest") mimetypes.add_type("model/gltf-binary", ".glb") REQUEST_ID_PATTERN = re.compile(r"^req-[a-z0-9-]+$") PUBLIC_AUTH_PATHS = { "/login", "/login.html", "/login.css", "/login.js", "/network.js", "/api/auth/status", "/api/auth/login", } NON_DISPATCH_DISPLAY_SOURCES = frozenset({"mode_editor_compile"}) class VehicleDisplaySync: """Process-local snapshot consumed by passive vehicle display clients.""" def __init__(self) -> None: self._lock = threading.Lock() self._revision = 0 self._snapshot: dict[str, object] = { "revision": 0, "updated_at": None, "task": { "status": "idle", "utterance": "", "source": "head_unit", "result": None, "error": None, }, "extension": { "catalog": [], "last_action": None, "result": None, }, } @staticmethod def _safe_copy(value: object) -> object: return json.loads(json.dumps(value, ensure_ascii=False, default=str)) def update_task( self, *, status: str, utterance: str, source: str = "head_unit", result: object = None, error: object = None, ) -> None: with self._lock: self._revision += 1 self._snapshot["revision"] = self._revision self._snapshot["updated_at"] = time.time() self._snapshot["task"] = { "status": status, "utterance": utterance, "source": source or "head_unit", "result": self._safe_copy(result), "error": self._safe_copy(error), } def update_extension( self, *, catalog: object | None = None, action: object | None = None, result: object | None = None, ) -> None: with self._lock: self._revision += 1 current = dict(self._snapshot.get("extension", {})) if catalog is not None: current["catalog"] = self._safe_copy(catalog) if action is not None: current["last_action"] = self._safe_copy(action) if result is not None: current["result"] = self._safe_copy(result) self._snapshot["revision"] = self._revision self._snapshot["updated_at"] = time.time() self._snapshot["extension"] = current def snapshot(self, health: dict[str, object]) -> dict[str, object]: with self._lock: payload = self._safe_copy(self._snapshot) assert isinstance(payload, dict) payload["extension_health"] = self._safe_copy(health) return payload def extension_catalog(self) -> list[dict[str, object]]: with self._lock: extension = self._snapshot.get("extension", {}) catalog = extension.get("catalog", []) if isinstance(extension, dict) else [] payload = self._safe_copy(catalog) if not isinstance(payload, list): return [] return [item for item in payload if isinstance(item, dict)] def _lan_ipv4_address() -> str | None: """Return the IPv4 address selected by the host's default route.""" probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: probe.connect(("192.0.2.1", 80)) address = str(probe.getsockname()[0]) return None if address.startswith("127.") else address except OSError: return None finally: probe.close() class SeatDemoHandler(BaseHTTPRequestHandler): service = SeatDemoService() # Codex defaults to its own empty temporary workspace. A deployment may # opt into another isolated directory through CODEX_ASSISTANT_WORKSPACE, # but it must never inherit access to this application's source tree. assistant = CodexAssistantService( CodexAppServerClient(), fast_implicit_controls=True, ) gesture = GestureCapabilityBridge(REPOSITORY_ROOT) extension = SelfExtensionService() nfc = NfcReaderService() pets = PetCardService( nfc_provider=nfc.snapshot, publisher=service.publish_pet_message, ) extension_toys = NfcExtensionToyPublisher( nfc_provider=nfc.snapshot, catalog_provider=extension.list_peripheral_capabilities, publisher=service.publish_extension_toy_message, ) extension_control = ExtensionControlConsumer( catalog_provider=extension.list_peripheral_capabilities, executor=extension.execute_bound_infrared_action, ) service.set_extension_control_handler(extension_control.submit) service.set_extension_control_rejection_handler( extension_control.reject_transport_message ) calibration = CalibrationService( REPOSITORY_ROOT / "artifacts" / "tmp" / "self-calibration", repository_root=REPOSITORY_ROOT, ) repair = SelfRepairService() speech = SpeechTranscriber(REPOSITORY_ROOT) # Reject excess live-preview work before reading request bodies. The # transcriber owns a second, stricter gate so non-HTTP callers are covered too. speech_preview_http_slots = threading.BoundedSemaphore(6) auth = AuthRuntime(AuthConfig.from_env()) vehicle_display_sync = VehicleDisplaySync() assistant_capability_catalog: AssistantCapabilityCatalog | None = None server_version = "CognitiveAgentCabinDemo/2.0" def do_GET(self) -> None: # noqa: N802 - stdlib handler API parsed_url = urlparse(self.path) path = unquote(parsed_url.path) if path == "/favicon.ico": self.send_response(HTTPStatus.NO_CONTENT) self._send_security_headers() self.end_headers() return if path == "/api/auth/status": username = self._session_username() self._json( { "enabled": self.auth.config.enabled, "authenticated": not self.auth.config.enabled or username is not None, "username": username, "session_ttl_seconds": self.auth.config.session_ttl_seconds, "requests_per_minute": self.auth.config.requests_per_minute, } ) return if path == "/api/health": assistant_status = self.assistant.status() assistant_available = bool(assistant_status.get("available")) self._json( { "status": "ok" if assistant_available else "unavailable", "service": "cabin-cognitive-orchestrator", "assistant": { "engine": str(assistant_status.get("engine") or "codex-app-server"), "status": str(assistant_status.get("status") or "unknown"), "available": assistant_available, }, }, HTTPStatus.OK if assistant_available else HTTPStatus.SERVICE_UNAVAILABLE, ) return if path == "/n7/scenes": vehicle_id = str(parse_qs(parsed_url.query).get("vehicle_id", [""])[0]) or None self._json( self.service.mode_scene_catalog(vehicle_id), headers={"Access-Control-Allow-Origin": "*"}, ) return if path in {"/login", "/login.html"}: if self.auth.config.enabled and self._session_username() is not None: self._redirect("/") return self._serve_static_file("login.html") return if not self._authorize(path): return if path in {"/hardware-control", "/hardware-control.html"}: if not self._require_local_decision_point(): return self._serve_static_file("hardware-control.html") return if path in {"/hardware-control.js", "/hardware-control.css"}: if not self._require_local_decision_point(): return self._serve_static_file(path.lstrip("/")) return if path == "/api/local/hardware-access": if not self._require_local_decision_point(): return self._json(self.service.hardware_access.admin_snapshot()) return if path == "/api/hardware/access/status": query = parse_qs(parsed_url.query) try: self._json( self.service.hardware_access.status( str(query.get("client_id", [""])[0]) ) ) except ValueError as exc: self._json( {"error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST, ) return if path == "/api/assistant/status": self._json(self.assistant.status()) return if path == "/api/assistant/capabilities": self._json(self._assistant_capability_catalog().snapshot()) return if path == "/api/mode-profiles/status": self._json(self.service.mode_profile_sync_status()) return if path == "/api/runtime": self._json(self.service.runtime_info()) return if path == "/api/modes/schedule/status": query = parse_qs(parsed_url.query) try: vehicle = self.service.vehicle_profiles.get(str(query.get("vehicle_id", [""])[0]) or None) self._json(public_schedule_status(self.service.mode_schedule_runner.status(vehicle.id))) except ValueError as exc: self._json({"error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST) return if path == "/api/vehicle-display-sync": self._json(self.vehicle_display_sync.snapshot(self._extension_health())) return if path == "/api/extension/control/status": query = parse_qs(parsed_url.query) try: after_revision = int(query.get("after_revision", ["-1"])[0]) timeout_ms = int(query.get("timeout_ms", ["25000"])[0]) if after_revision < -1: raise ValueError("after_revision must be at least -1") if not 0 <= timeout_ms <= 30_000: raise ValueError("timeout_ms must be between 0 and 30000") snapshot = self.extension_control.wait_for_snapshot( after_revision, timeout_s=timeout_ms / 1_000, ) except (TypeError, ValueError) as exc: self._json( {"error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST, ) return self._json(snapshot) return if path in {"/api/hardware/seat-state", "/api/hardware/vehicle-state"}: query = parse_qs(parsed_url.query) conversation_id = str(query.get("conversation_id", [""])[0]) vehicle_id = str(query.get("vehicle_id", [""])[0]) or None control_transport = str( query.get("control_transport", ["wired_can"])[0] ) hardware_client_id = str(query.get("hardware_client_id", [""])[0]) or None try: self._json( ( self.service.synchronize_vehicle_state if path == "/api/hardware/vehicle-state" else self.service.synchronize_hardware_state )( conversation_id, vehicle_id, control_transport=control_transport, hardware_client_id=hardware_client_id, hardware_access_bypass=( self._passive_state_access_bypass_allowed( control_transport ) ), ) ) 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, {"Retry-After": str(exc.retry_after_seconds)}, ) except ValueError as exc: self._json( {"error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST, ) except Exception as exc: # noqa: BLE001 - surface passive CAN availability self.log_error("physical CAN state synchronization failed: %s", exc) self._json( { "error": "hardware_state_unavailable", "message": str(exc), }, HTTPStatus.SERVICE_UNAVAILABLE, ) return if path == "/api/gesture/runtime": self._json(self.gesture.runtime_info()) return if path == "/api/gesture/model": self._serve_gesture_model() return if path == "/api/extension/health": self._json(self._extension_health()) return if path == "/api/extension/nfc/status": self._json( { "success": True, **self.nfc.snapshot(), "pet_cards": self.pets.snapshot(), "extension_toy": self.extension_toys.snapshot(), } ) return if path == "/api/extension/pets": self._json({"success": True, **self.pets.snapshot()}) return if path == "/api/extension/gscu/slots": try: self._json(self.extension.list_infrared_slots()) except GatewayError as exc: self._json( {"success": False, "error": "catalog_unavailable", "message": str(exc)}, HTTPStatus.SERVICE_UNAVAILABLE, ) return if path == "/api/extension/peripherals": try: catalog = self.extension.list_peripheral_capabilities() self._json( { "success": True, "catalog": catalog, "count": len(catalog), } ) except GatewayError as exc: self._json( { "success": False, "error": "catalog_unavailable", "message": str(exc), }, HTTPStatus.SERVICE_UNAVAILABLE, ) return if path == "/api/extension/gscu/info": self._handle_extension_gateway("info", {}) return if path.startswith("/api/calibration/"): if not self._require_calibration_admin(parsed_url): return if path == "/api/calibration/state": self._json(self.calibration.snapshot()) return if path == "/api/calibration/events": self._serve_calibration_events() return if path == "/api/calibration/video/stream": self._serve_calibration_video() return self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND) return if path.startswith("/api/self-repair/"): if not self._require_self_repair_admin(parsed_url): return if path == "/api/self-repair/state": self._json(self.repair.snapshot()) return if path == "/api/self-repair/health": self._json(self.repair.health()) return self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND) return if path == "/api/speech/status": self._json(self.speech.status()) return if path.startswith("/api/requests/"): self._serve_request(path.removeprefix("/api/requests/")) return self._serve_static(path, parsed_url.query) def do_OPTIONS(self) -> None: # noqa: N802 - stdlib handler API path = unquote(urlparse(self.path).path) if path != "/n7/scenes": self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND) return self.send_response(HTTPStatus.NO_CONTENT) self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS") self.send_header("Access-Control-Allow-Headers", "Accept, Content-Type") self.send_header("Content-Length", "0") self.send_header("Cache-Control", "no-store") self._send_security_headers() self.end_headers() def do_POST(self) -> None: # noqa: N802 - stdlib handler API path = unquote(urlparse(self.path).path) if not self._require_same_origin(): return if path == "/api/internal/codex/vehicle-control": if not self._require_json_content_type(): return self._handle_codex_vehicle_control() return if path == "/api/auth/login": if not self._require_json_content_type(): return self._login() return if path == "/api/auth/logout": self._logout() return if not self._authorize(path): return if path == "/api/assistant/stream": if not self._require_json_content_type(): return self._handle_assistant_stream() return if path == "/api/speech/transcribe": self._handle_speech_transcription() return if path == "/api/speech/preview": self._handle_speech_preview() return if not self._require_json_content_type(): return if path in { "/api/hardware/access/request", "/api/hardware/access/release", "/api/local/hardware-access/approve", "/api/local/hardware-access/deny", "/api/local/hardware-access/revoke", }: self._handle_hardware_access_post(path) return if path.startswith("/api/extension/"): self._handle_extension_post(path) return if path.startswith("/api/calibration/"): self._handle_calibration_post(path) return if path.startswith("/api/self-repair/"): self._handle_self_repair_post(path) return if path == "/api/mode-profile-drafts/trial": self._handle_mode_profile_draft_trial() return if path == "/api/mode-profiles" or path.startswith("/api/mode-profiles/"): self._handle_mode_profile_post(path) return supplement_match = re.fullmatch( r"/api/requests/(?Preq-[a-z0-9-]+)/supplement", path, ) if ( path not in { "/api/requests", "/api/requests/preview", "/api/can/commands", } and not supplement_match ): self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND) return utterance = "" source = "head_unit" sync_vehicle_task = True def fail_display_sync(code: str, error: Exception) -> None: if not utterance or not sync_vehicle_task: return self.vehicle_display_sync.update_task( status="failed", utterance=utterance, source=source, error={ "code": code, "message": str(error), "outcome": "failed_or_blocked", }, ) try: username = self._session_username() or "local" retry_after = self.auth.api_requests.retry_after(username) if self.auth.config.enabled and retry_after: self._json( { "error": "rate_limited", "message": "请求过于频繁,请稍后再试。", }, HTTPStatus.TOO_MANY_REQUESTS, {"Retry-After": str(retry_after)}, ) return if self.auth.config.enabled: self.auth.api_requests.record(username) payload = self._read_json_body() direct_can_commands = ( payload.get("commands") if path == "/api/can/commands" else None ) utterance = ( ";".join( item if isinstance(item, str) else repr(item) for item in direct_can_commands ) if isinstance(direct_can_commands, list) else str(payload.get("utterance", "")) ) source = str(payload.get("client_type", "head_unit")) sync_vehicle_task = source not in NON_DISPATCH_DISPLAY_SOURCES if utterance and sync_vehicle_task: self.vehicle_display_sync.update_task( status="running", utterance=utterance, source=source, ) hardware_client_id = ( str(payload["hardware_client_id"]) if payload.get("hardware_client_id") else None ) reasoning_effort = ( str(payload["reasoning_effort"]) if payload.get("reasoning_effort") is not None else None ) provider = str(payload.get("provider", "auto")) execution_mode = str(payload.get("execution_mode", "dry_run")) conversation_id = ( str(payload["conversation_id"]) if payload.get("conversation_id") else None ) common_arguments = { "utterance": utterance, "provider": provider, "conversation_id": conversation_id, "execution_mode": execution_mode, # Hardware authorization is a server deployment policy. The # browser never receives or transmits this secret. "approval_token": os.getenv( "COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN" ), "vehicle_id": ( str(payload["vehicle_id"]) if payload.get("vehicle_id") else None ), "simulation_fault": str( payload.get("simulation_fault", "normal") ), "hardware_client_id": hardware_client_id, "hardware_access_bypass": self._hardware_access_bypass_allowed(), "reasoning_effort": reasoning_effort, "control_transport": str( payload.get("control_transport", "wired_can") ), } speech_utterance_id = str( payload.get("speech_utterance_id", "") ).strip() speech_understanding: dict[str, object] | None = None speech_peripheral_route = False if speech_utterance_id: if path != "/api/requests": raise ValueError( "speech semantic execution is only accepted by /api/requests" ) speech_understanding = self.speech.cached_result( speech_utterance_id ) clean_text = str( speech_understanding.get("clean_text", "") ).strip() raw_text = str( speech_understanding.get("raw_text", "") ).strip() peripheral_routing = self._speech_peripheral_routing(raw_text) peripheral_transcript = bool( peripheral_routing.get("routing_eligible") and raw_text ) expected_text = raw_text if peripheral_transcript else clean_text if utterance != expected_text: raise SpeechRecognitionError( "speech_text_changed", "语音文本已被修改,请确认修改后的内容再执行。", status=409, ) speech_peripheral_route = peripheral_transcript if ( speech_understanding.get("execution_eligible") is not True and not speech_peripheral_route ): raise SpeechRecognitionError( "speech_not_executable", "语音意图尚未通过能力或参数校验,已拒绝执行。", status=422, ) provider = "local" common_arguments["provider"] = provider if not speech_peripheral_route: common_arguments["_speech_understanding"] = speech_understanding if ( path != "/api/can/commands" and "application/x-ndjson" in self.headers.get("Accept", "") ): self._stream_control_request( path, supplement_match, payload, common_arguments, source=source, ) return if path == "/api/can/commands": result = self.service.process_can_commands( direct_can_commands, conversation_id=( str(payload["conversation_id"]) if payload.get("conversation_id") else None ), execution_mode=str(payload.get("execution_mode", "dry_run")), approval_token=os.getenv( "COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN" ), vehicle_id=( str(payload["vehicle_id"]) if payload.get("vehicle_id") else None ), simulation_fault=str( payload.get("simulation_fault", "normal") ), hardware_client_id=hardware_client_id, hardware_access_bypass=self._hardware_access_bypass_allowed(), control_transport=str( payload.get("control_transport", "wired_can") ), ) else: result = None if path in {"/api/requests", "/api/requests/preview"}: result = self._execute_request_with_peripherals( path, common_arguments, ) if result is None: if path == "/api/requests/preview": result = self.service.preview_native_can(**common_arguments) elif supplement_match: result = self.service.supplement_native_can( supplement_match.group("request_id"), provider=( str(payload["provider"]) if payload.get("provider") else None ), approval_token=os.getenv( "COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN" ), hardware_client_id=hardware_client_id, hardware_access_bypass=self._hardware_access_bypass_allowed(), reasoning_effort=reasoning_effort, ) else: result = self.service.process(**common_arguments) if speech_peripheral_route and speech_understanding is not None: result["speech_understanding"] = { "utterance_id": speech_utterance_id, "raw_text": speech_understanding.get("raw_text"), "clean_text": speech_understanding.get("clean_text"), "semantic_status": speech_understanding.get("semantic_status"), "routing": "validated_peripheral_text", } utterance = str(result.get("utterance") or utterance) if sync_vehicle_task: self.vehicle_display_sync.update_task( status=str(result.get("status", "succeeded")), utterance=utterance, source=source, result=result, ) self._json(result, HTTPStatus.CREATED) except HardwareAccessRequiredError as exc: fail_display_sync("hardware_access_required", exc) self._json( { "error": "hardware_access_required", "message": str(exc), "outcome": "failed_or_blocked", "access_state": exc.state, }, HTTPStatus.FORBIDDEN, ) except HardwareBusyError as exc: fail_display_sync("hardware_busy", exc) self._json( { "error": "hardware_busy", "message": str(exc), "outcome": "failed_or_blocked", "retry_after_seconds": exc.retry_after_seconds, }, HTTPStatus.CONFLICT, {"Retry-After": str(exc.retry_after_seconds)}, ) except HardwareSafetyError as exc: fail_display_sync("hardware_safety_gate", exc) self._json( { "error": "hardware_safety_gate", "message": str(exc), "outcome": "failed_or_blocked", }, HTTPStatus.FORBIDDEN, ) except CanGatewayError as exc: fail_display_sync("can_transport_failed", exc) self._json( { "error": "can_transport_failed", "message": str(exc), "outcome": "failed_or_blocked", }, HTTPStatus.SERVICE_UNAVAILABLE, ) except GatewayError as exc: fail_display_sync("gscu_unavailable", exc) self._json( {"error": "gscu_unavailable", "message": str(exc)}, HTTPStatus.SERVICE_UNAVAILABLE, ) except ExtensionIntegrationError as exc: fail_display_sync("extension_request_ambiguous", exc) self._json( {"error": "extension_request_ambiguous", "message": str(exc)}, HTTPStatus.UNPROCESSABLE_ENTITY, ) except SpeechRecognitionError as exc: fail_display_sync(exc.code, exc) self._json( {"error": exc.code, "message": str(exc)}, HTTPStatus(exc.status), ) except (ValueError, json.JSONDecodeError) as exc: fail_display_sync("invalid_request", exc) self._json( { "error": "invalid_request", "message": str(exc), "outcome": "failed_or_blocked", }, HTTPStatus.BAD_REQUEST, ) except OpenAIIntentError as exc: fail_display_sync("openai_request_failed", exc) self.log_error("OpenAI intent request failed: %s", exc) self._json( { "error": "openai_request_failed", "message": "GPT 意图解析失败,请检查 API Key、模型权限或网络状态。", "outcome": "failed_or_blocked", }, HTTPStatus.BAD_GATEWAY, ) except Exception as exc: # noqa: BLE001 - keep the demo API responsive fail_display_sync("internal_error", exc) self.log_error("request failed: %s", exc) self._json( { "error": "internal_error", "message": "请求处理失败,请查看服务端日志。", "outcome": "failed_or_blocked", "detail": str(exc), }, HTTPStatus.INTERNAL_SERVER_ERROR, ) def _peripheral_routing_catalog(self) -> list[dict[str, object]]: """Read the parse catalog while remaining compatible with test doubles.""" provider = getattr( self.extension, "list_peripheral_routing_capabilities", None, ) if callable(provider): return list(provider()) return list(self.extension.list_peripheral_capabilities()) def _execute_active_peripheral_request( self, utterance: str, *, provider: str, conversation_id: str | None, execution_mode: str, on_commands_validated: Callable[[dict[str, object]], None] | None = None, natural_language_routing: Mapping[str, object] | None = None, ) -> dict[str, object] | None: """Route a command to a persistently NFC-bound peripheral ability.""" # The browser may disappear or reload between learning and execution. # Natural-language routing therefore reads the durable, validated # whitelist instead of trusting the display-sync snapshot. extension_catalog = self.extension.list_peripheral_capabilities() nfc_snapshot = self.nfc.snapshot() plan_catalog = active_peripheral_plan_catalog( utterance, extension_catalog, nfc_snapshot, ) peripheral = None if not plan_catalog: peripheral = resolve_active_peripheral_command( utterance, extension_catalog, nfc_snapshot, ) if not plan_catalog and peripheral is None: return None if conversation_id and not re.fullmatch( r"conv-[a-z0-9-]+", conversation_id ): raise ValueError( "conversation_id must start with conv- and contain only " "lowercase letters, numbers, or hyphens" ) conversation_turn = 1 if conversation_id: context = self.service.conversations.snapshot(conversation_id) conversation_turn = int(context.get("turn_count", 0)) + 1 if plan_catalog: plan_result = self.extension.plan( { "utterance": utterance, "peripherals": plan_catalog, "vehicle_devices": [], "provider": provider, **( { "natural_language_routing": dict( natural_language_routing ) } if natural_language_routing is not None else {} ), } ) if plan_result.get("needs_confirmation"): raise ExtensionIntegrationError( str(plan_result.get("question") or "请明确要控制的外设。") ) result = self.extension.execute_peripheral_plan_request( utterance, plan_result, plan_catalog, conversation_id=conversation_id, conversation_turn=conversation_turn, execution_mode=execution_mode, provider_requested=provider, on_commands_validated=on_commands_validated, ) self.vehicle_display_sync.update_extension( action={ "type": "sequence_emit", "source": "main_orchestration", "step_count": len(plan_result.get("steps", [])), }, result=result, ) return result assert peripheral is not None result = self.extension.execute_peripheral_request( utterance, peripheral, conversation_id=conversation_id, conversation_turn=conversation_turn, execution_mode=execution_mode, provider_requested=provider, on_commands_validated=on_commands_validated, ) self.vehicle_display_sync.update_extension( action={ "type": "emit", "source": "main_orchestration", "slot": peripheral["slot"], "capability_id": peripheral.get("id"), }, result=result, ) return result def _speech_peripheral_routing(self, utterance: str) -> dict[str, object]: """Validate whether an ASR transcript may enter peripheral routing. Vehicle speech keeps using its cached structured semantic payload. A learned peripheral cannot be represented by the static vehicle capability catalog, so it may instead reuse the exact server-owned ASR transcript once the learned device and action resolve. Execution authorization is checked separately so an unbound device can be shown as parsed-and-blocked without ever reaching the infrared transport. """ text = str(utterance or "").strip() if not text: return {"execution_eligible": False, "action_count": 0} try: catalog = self._peripheral_routing_catalog() nfc_snapshot = self.nfc.snapshot() partition = partition_active_peripheral_intent( text, catalog, nfc_snapshot, ) entries = list(partition.get("peripheral_actions") or []) unresolved = list( partition.get("unresolved_peripheral_clauses") or [] ) denied = list(partition.get("denied_peripheral_actions") or []) plan_catalog = active_peripheral_plan_catalog( text, catalog, nfc_snapshot, ) direct = None if not entries and not plan_catalog and not unresolved and not denied: direct = resolve_active_peripheral_command( text, catalog, nfc_snapshot, ) except (ExtensionIntegrationError, GatewayError, ValueError): return {"execution_eligible": False, "action_count": 0} action_count = len(entries) or (1 if direct is not None else 0) if plan_catalog: action_count = max(action_count, 1) routing_eligible = bool(action_count or unresolved or denied) return { "execution_eligible": bool(action_count and not unresolved and not denied), "routing_eligible": routing_eligible, "action_count": action_count, "denied_action_count": len(denied), "has_ordered_plan": bool(plan_catalog), } @staticmethod def _residual_has_vehicle_language(utterance: str) -> bool: """Recognize only obvious vehicle-domain text in a claimed mixed request.""" return bool( re.search( r"空调|温度|风量|循环|主驾|副驾|座椅|车窗|车门|后门|" r"遮阳帘|阅读灯|氛围灯|室内灯|出风口|风口|" r"\b(?:ac|seat|window|door|shade|light|outlet)\b", utterance, flags=re.IGNORECASE, ) ) def _execute_local_peripheral_actions( self, utterance: str, entries: list[dict[str, object]], *, provider: str, conversation_id: str | None, execution_mode: str, on_commands_validated: Callable[[dict[str, object]], None] | None = None, ) -> dict[str, object]: """Execute already validated infrared capabilities.""" capabilities = [ { **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("没有可执行的已学习红外能力。") conversation_turn = 1 if conversation_id: context = self.service.conversations.snapshot(conversation_id) conversation_turn = int(context.get("turn_count", 0)) + 1 if len(capabilities) == 1: result = self.extension.execute_peripheral_request( utterance, capabilities[0], conversation_id=conversation_id, conversation_turn=conversation_turn, execution_mode=execution_mode, provider_requested=provider, on_commands_validated=on_commands_validated, ) else: result = self.extension.execute_peripheral_plan_request( utterance, { "summary": f"本地规则命中 {len(capabilities)} 个已学习红外动作。", "provider_used": "infrared_local_rules", "steps": [ { "kind": "peripheral", "capability_id": capability["id"], } for capability in capabilities ], }, capabilities, conversation_id=conversation_id, conversation_turn=conversation_turn, execution_mode=execution_mode, provider_requested=provider, on_commands_validated=on_commands_validated, ) result["provider_used"] = "infrared_local_rules" self.vehicle_display_sync.update_extension( action={ "type": "emit_sequence" if len(capabilities) > 1 else "emit", "source": "main_orchestration_local_rule", "capability_ids": [item["id"] for item in capabilities], "slots": [item["slot"] for item in capabilities], }, result=result, ) return result @staticmethod def _merge_can_and_peripheral_results( utterance: str, can_result: dict[str, object], infrared_result: dict[str, object], entries: list[dict[str, object]], ) -> dict[str, object]: """Create one response while retaining each transport's evidence.""" merged = deepcopy(can_result) can_status = str(can_result.get("status") or "failed") infrared_status = str(infrared_result.get("status") or "failed") successful = {"succeeded", "partially_succeeded", "no_action"} can_ok = can_status in successful infrared_ok = infrared_status in successful function_configs = deepcopy(can_result.get("function_configs") or {}) function_configs.update( deepcopy(infrared_result.get("function_configs") or {}) ) command_plans = deepcopy(can_result.get("command_plans") or {}) command_plans.update(deepcopy(infrared_result.get("command_plans") or {})) trace_events = [ *deepcopy(list(infrared_result.get("trace_events") or [])), *deepcopy(list(can_result.get("trace_events") or [])), ] can_operations = list(can_result.get("operation_results") or []) infrared_operations = list(infrared_result.get("operation_results") or []) operation_results = [*can_operations, *infrared_operations] can_report = deepcopy(can_result.get("execution_report")) infrared_report = deepcopy(infrared_result.get("execution_report")) execution_report = can_report or infrared_report if isinstance(execution_report, dict): spec = execution_report.setdefault("spec", {}) if execution_report is can_report and isinstance(infrared_report, dict): spec["results"] = [ *list(spec.get("results") or []), *list((infrared_report.get("spec") or {}).get("results") or []), ] spec["status"] = ( "succeeded" if can_ok and infrared_ok else "partially_succeeded" ) metadata = execution_report.setdefault("metadata", {}) labels = metadata.setdefault("labels", {}) labels["route"] = "unified_can_infrared" infrared_actions = [ { "index": entry.get("index"), "clause": entry.get("clause"), "capability_id": capability.get("id"), "device_id": capability.get("device_id"), "device_name": capability.get("device_name"), "action": capability.get("action"), "slot": capability.get("slot"), "parse_source": str( capability.get("parse_source") or "local_rule" ), "transport": str(infrared_result.get("control_transport") or "gscu_tcp"), } for entry in entries for capability in [entry.get("capability")] if isinstance(capability, dict) ] merged.update( { "utterance": utterance, "status": ( "succeeded" if can_ok and infrared_ok else "partially_succeeded" if can_ok or infrared_ok else "failed" ), "success": can_ok and infrared_ok, "orchestration_route": "unified_can_infrared", "provider_used": ( f"{can_result.get('provider_used') or 'vehicle'}+" f"{infrared_result.get('provider_used') or 'infrared_local_rules'}" ), "decision_summary": [ *list(can_result.get("decision_summary") or []), *list(infrared_result.get("decision_summary") or []), "CAN 与红外动作已通过独立白名单校验并按各自传输链路执行。", ], "function_configs": function_configs, "command_plans": command_plans, "function_config": next(iter(function_configs.values()), None), "command_plan": next(iter(command_plans.values()), None), "trace_events": trace_events, "execution_report": execution_report, "operation_results": operation_results, "infrared_actions": infrared_actions, "peripheral": infrared_result.get("peripheral"), "control_transports": { "vehicle": can_result.get("control_transport"), "infrared": infrared_result.get("control_transport"), }, "phase_execution_reports": { **dict(can_result.get("phase_execution_reports") or {}), "vehicle": can_report, "infrared": infrared_report, }, } ) return merged @staticmethod def _merge_peripheral_phase_results( utterance: str, local_result: dict[str, object], cloud_result: dict[str, object], ) -> dict[str, object]: """Merge local and hosted IR phases without replaying local actions.""" merged = deepcopy(cloud_result) def phase_documents( local_documents: object, cloud_documents: object, ) -> dict[str, object]: local_mapping = local_documents if isinstance(local_documents, Mapping) else {} cloud_mapping = cloud_documents if isinstance(cloud_documents, Mapping) else {} return { **{ f"local_{key}": deepcopy(value) for key, value in local_mapping.items() }, **{ f"cloud_{key}": deepcopy(value) for key, value in cloud_mapping.items() }, } function_configs = phase_documents( local_result.get("function_configs"), cloud_result.get("function_configs"), ) command_plans = phase_documents( local_result.get("command_plans"), cloud_result.get("command_plans"), ) successful = {"succeeded", "no_action"} local_ok = str(local_result.get("status") or "") in successful cloud_ok = str(cloud_result.get("status") or "") in successful local_request_id = str(local_result.get("request_id") or "") cloud_request_id = str(cloud_result.get("request_id") or "") merged.update( { "utterance": utterance, "status": ( "succeeded" if local_ok and cloud_ok else "partially_succeeded" if local_ok or cloud_ok else "failed" ), "success": local_ok and cloud_ok, "orchestration_route": "two_stage_nfc_peripheral", "provider_used": ( f"{local_result.get('provider_used') or 'infrared_local_rules'}+" f"{cloud_result.get('provider_used') or 'cloud_model'}" ), "preview_request_id": local_request_id, "supplement_of": local_request_id, "supplement_request_id": cloud_request_id, "decision_summary": [ *list(local_result.get("decision_summary") or []), *list(cloud_result.get("decision_summary") or []), "本地红外动作先行下发;云端只补充未解析子句。", ], "function_configs": function_configs, "command_plans": command_plans, "function_config": next(iter(function_configs.values()), None), "command_plan": next(iter(command_plans.values()), None), "trace_events": [ *deepcopy(list(local_result.get("trace_events") or [])), *deepcopy(list(cloud_result.get("trace_events") or [])), ], "validation": [ *deepcopy(list(local_result.get("validation") or [])), *deepcopy(list(cloud_result.get("validation") or [])), ], "operation_results": [ *deepcopy(list(local_result.get("operation_results") or [])), *deepcopy(list(cloud_result.get("operation_results") or [])), ], "phase_execution_reports": { "infrared_local": deepcopy(local_result.get("execution_report")), "infrared_cloud": deepcopy(cloud_result.get("execution_report")), }, "execution_report": deepcopy(cloud_result.get("execution_report")) or deepcopy(local_result.get("execution_report")), "peripheral": deepcopy( cloud_result.get("peripheral") or local_result.get("peripheral") ), } ) for key in ( "execution_mode", "control_transport", "local_control_budget_ms", "local_control_elapsed_ms", "local_control_within_budget", "natural_language_routing", ): if local_result.get(key) is not None: merged[key] = deepcopy(local_result[key]) return merged @staticmethod def _apply_nfc_command_denials( result: dict[str, object], entries: list[dict[str, object]], ) -> dict[str, object]: """Expose parsed-but-unauthorized IR commands without dispatching them.""" if not entries: return result denied_actions = [ { "index": entry.get("index"), "clause": entry.get("clause"), "capability_id": capability.get("id"), "device_id": capability.get("device_id"), "device_name": capability.get("device_name"), "action": capability.get("action"), "slot": capability.get("slot"), "reason": str(entry.get("reason") or "nfc_command_disabled"), } for entry in entries for capability in [entry.get("capability")] if isinstance(entry, dict) and isinstance(capability, dict) ] if not denied_actions: return result result["denied_peripheral_actions"] = denied_actions result["authorization_filtered"] = True blocked_operations: list[dict[str, object]] = [] blocked_commands: list[dict[str, object]] = [] blocked_results: list[dict[str, object]] = [] for position, item in enumerate(denied_actions, start=1): reason = str(item.get("reason") or "nfc_command_disabled") device_name = str( item.get("device_name") or item.get("device_id") or "未命名外设" ) action_name = str(item.get("action") or "未命名动作") operation_ref = ( f"{item.get('capability_id') or 'infrared'}-authorization-{position}" ) message = ( f"已识别“{device_name} · {action_name}”,但设备尚未完成 NFC " "绑定授权,未下发。" if reason == "nfc_binding_required" else f"已识别“{device_name} · {action_name}”,但该 NFC 能力已被禁用,未下发。" ) blocked_operations.append( { "id": operation_ref, "capability": "infrared.emit", "target": { "device": item.get("device_id"), "name": item.get("device_name"), }, "parameters": { "action": item.get("action"), "slot": item.get("slot"), }, "parse_source": "local_rule", "authorization": { "authorized": False, "reason": reason, }, } ) blocked_commands.append( { "operation_ref": operation_ref, "bus": "GSCU_TCP", "message": "infrared.emit", "signals": { "slot": item.get("slot"), "authorization_status": "blocked", "authorization_reason": reason, }, } ) blocked_results.append( { "ref": operation_ref, "domain": "self_extension", "status": "blocked", "message": message, "telemetry": { "transport": "gscu_tcp", "slot": item.get("slot"), "delivery_status": "not_submitted", "transport_submitted": False, "gateway_acknowledged": False, "physical_effect_confirmed": False, "verification_status": "failed_or_blocked", "authorization_reason": reason, }, } ) function_configs = deepcopy(result.get("function_configs") or {}) command_plans = deepcopy(result.get("command_plans") or {}) config_key = "self_extension_blocked" suffix = 2 while config_key in function_configs or config_key in command_plans: config_key = f"self_extension_blocked_{suffix}" suffix += 1 blocked_config = { "apiVersion": "cognitive-agent.io/v1alpha1", "kind": "FunctionConfig", "metadata": { "name": config_key, "labels": { "execution_scope": "nfc_authorization_required", }, }, "spec": { "domain": "self_extension", "operations": blocked_operations, }, } blocked_plan = { "apiVersion": "cognitive-agent.io/v1alpha1", "kind": "CommandPlan", "metadata": { "name": f"{config_key}-gscu", "labels": {"authorization_status": "blocked"}, }, "spec": { "domain": "self_extension", "platform": "gscu1b", "commands": blocked_commands, }, } function_configs[config_key] = blocked_config command_plans[config_key] = blocked_plan result["function_configs"] = function_configs result["command_plans"] = command_plans if not isinstance(result.get("function_config"), dict): result["function_config"] = blocked_config if not isinstance(result.get("command_plan"), dict): result["command_plan"] = blocked_plan result["operation_results"] = [ *deepcopy(list(result.get("operation_results") or [])), *blocked_results, ] result["trace_events"] = [ *deepcopy(list(result.get("trace_events") or [])), { "stage": "nfc_peripheral_authorization", "status": "blocked", "summary": "红外指令已由本地规则解析,但未通过 NFC 执行授权。", "duration_ms": 0, }, ] summaries = list(result.get("decision_summary") or []) binding_required = [ item for item in denied_actions if item.get("reason") == "nfc_binding_required" ] disabled = [ item for item in denied_actions if item.get("reason") != "nfc_binding_required" ] if binding_required: labels = "、".join( str(item.get("clause") or item.get("action") or "未命名动作") for item in binding_required ) summaries.append( f"已解析 {len(binding_required)} 条红外控制指令,但设备尚未完成 NFC " f"绑定授权,未下发:{labels}。" ) if disabled: labels = "、".join( str(item.get("clause") or item.get("action") or "未命名动作") for item in disabled ) summaries.append( f"持久绑定中有 {len(disabled)} 条红外能力未启用,已过滤:{labels}。" ) result["decision_summary"] = summaries prior_status = str(result.get("status") or "failed") if prior_status in {"succeeded", "partially_succeeded", "no_action"}: result["status"] = "partially_succeeded" result["success"] = False result["outcome"] = "failed_or_blocked" execution_report = result.get("execution_report") if isinstance(execution_report, dict): spec = execution_report.get("spec") if isinstance(spec, dict): spec["status"] = str(result.get("status") or "blocked") return result def _exact_n7_mode_invocation( self, utterance: str, vehicle_id: str | None, ) -> object | None: """Resolve a standalone spoken/text invocation of one N7-synced card.""" if not str(utterance).strip(): return None registry = getattr(self.service, "mode_profiles", None) match_text = getattr(registry, "match_text", None) if not callable(match_text): # Lightweight service doubles used by peripheral-only routes do # not own a mode registry. They must keep their existing routing # behaviour instead of failing before the peripheral parser runs. return None matches = [ profile for profile in match_text( utterance, vehicle_id=vehicle_id, ) if getattr(profile, "sync_origin", "") == "from_n7/scenes" ] # ``ModeProfileRegistry.match_text`` applies the bounded, exact # invocation grammar to N7-synchronised cards. Keeping that rule in # one place prevents text, speech and card-click routes from drifting. return matches[0] if len(matches) == 1 else None def _execute_n7_named_mode_request( self, path: str, common_arguments: dict[str, object], ) -> dict[str, object] | None: utterance = str(common_arguments.get("utterance") or "").strip() vehicle_id = ( str(common_arguments["vehicle_id"]) if common_arguments.get("vehicle_id") else None ) profile = self._exact_n7_mode_invocation(utterance, vehicle_id) if profile is None: return None execution_mode = ( "dry_run" if path == "/api/requests/preview" else str(common_arguments.get("execution_mode") or "dry_run") ) conversation_id = ( str(common_arguments["conversation_id"]) if common_arguments.get("conversation_id") else None ) entries = self._saved_mode_infrared_entries(profile) can_result = self.service.execute_mode_profile_card( str(getattr(profile, "id")), conversation_id=conversation_id, execution_mode=execution_mode, approval_token=( str(common_arguments["approval_token"]) if common_arguments.get("approval_token") else None ), vehicle_id=vehicle_id, simulation_fault=str( common_arguments.get("simulation_fault") or "normal" ), hardware_client_id=( str(common_arguments["hardware_client_id"]) if common_arguments.get("hardware_client_id") else None ), hardware_access_bypass=bool( common_arguments.get("hardware_access_bypass", True) ), control_transport=str( common_arguments.get("control_transport") or "wired_can" ), ) if not entries: can_result["utterance"] = utterance return can_result 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) return result def _execute_request_with_peripherals( self, path: str, common_arguments: dict[str, object], *, on_commands_validated: object | None = None, ) -> dict[str, object] | None: """Route IR-only or mixed IR/CAN requests without changing CAN parsing.""" local_stage_started = time.perf_counter() utterance = str(common_arguments.get("utterance") or "").strip() if self.service.is_mode_profile_configuration_request(utterance): # Creating, saving, updating, or restoring a reusable mode is # configuration, never a live-control command. Bypass the # NFC/peripheral executor so a learned device name embedded in the # mode sentence cannot claim it before SeatDemoService handles it. mode_arguments = { **common_arguments, "execution_mode": "dry_run", } callback = ( on_commands_validated if callable(on_commands_validated) else None ) if path == "/api/requests/preview": return self.service.preview_native_can( **mode_arguments, on_commands_validated=callback, ) return self.service.process( **mode_arguments, _on_commands_validated=callback, ) n7_mode_result = self._execute_n7_named_mode_request( path, common_arguments, ) if n7_mode_result is not None: return n7_mode_result catalog = self._peripheral_routing_catalog() nfc_snapshot = self.nfc.snapshot() partition = partition_active_peripheral_intent( utterance, catalog, nfc_snapshot, ) entries = list(partition.get("peripheral_actions") or []) residual = str(partition.get("residual_utterance") or "").strip() unresolved = list(partition.get("unresolved_peripheral_clauses") or []) denied = list(partition.get("denied_peripheral_actions") or []) parsed_peripheral_clauses = list( dict.fromkeys( str(entry.get("clause") or "").strip() for entry in [*entries, *denied] if isinstance(entry, dict) and str(entry.get("clause") or "").strip() ) ) unresolved_peripheral_clauses = list( dict.fromkeys(str(item).strip() for item in unresolved if str(item).strip()) ) natural_language_routing: dict[str, object] = { "original_utterance": utterance, "parsed_clauses": parsed_peripheral_clauses, "resolved_clauses": parsed_peripheral_clauses, "unresolved_clauses": unresolved_peripheral_clauses, } provider = str(common_arguments.get("provider") or "auto") conversation_id = ( str(common_arguments["conversation_id"]) if common_arguments.get("conversation_id") else None ) execution_mode = str(common_arguments.get("execution_mode") or "dry_run") callback = on_commands_validated if callable(on_commands_validated) else None peripheral_validation_published = False peripheral_validation_elapsed_ms: float | None = None def publish_peripheral_validation(result: dict[str, object]) -> None: nonlocal peripheral_validation_elapsed_ms, peripheral_validation_published peripheral_validation_published = True peripheral_validation_elapsed_ms = round( (time.perf_counter() - local_stage_started) * 1000, 3, ) if callback is None: return validated = deepcopy(result) validated["status"] = "processing" validated["phase"] = "commands_validated" validated.update( { "local_control_budget_ms": LOCAL_CONTROL_BUDGET_MS, "local_control_elapsed_ms": peripheral_validation_elapsed_ms, "local_control_within_budget": ( peripheral_validation_elapsed_ms < LOCAL_CONTROL_BUDGET_MS ), "natural_language_routing": deepcopy( natural_language_routing ), } ) callback(validated) infrared_result: dict[str, object] | None = None can_result: dict[str, object] | None = None cloud_supplement_error: str | None = None # Preserve the legacy all-peripheral planner when there is no hosted # ambiguity to resolve. It retains explicit delay/order semantics. if ( entries and residual and not unresolved and not self._residual_has_vehicle_language(residual) and bool( re.search( r"然后|随后|接着|之后|" r"(?:\d+(?:\.\d+)?|[一二三四五六七八九十两半]+)" r"(?:毫秒|秒(?:钟)?|分钟|小时|ms(?![A-Za-z])|s(?![A-Za-z]))", residual, flags=re.IGNORECASE, ) ) ): planned = self._execute_active_peripheral_request( utterance, provider=provider, conversation_id=conversation_id, execution_mode=execution_mode, on_commands_validated=publish_peripheral_validation, natural_language_routing=natural_language_routing, ) if planned is not None: if not peripheral_validation_published: publish_peripheral_validation(planned) if peripheral_validation_elapsed_ms is not None: planned.update( { "local_control_budget_ms": LOCAL_CONTROL_BUDGET_MS, "local_control_elapsed_ms": ( peripheral_validation_elapsed_ms ), "local_control_within_budget": ( peripheral_validation_elapsed_ms < LOCAL_CONTROL_BUDGET_MS ), "natural_language_routing": deepcopy( natural_language_routing ), } ) return planned if not entries and not denied and not unresolved: result = self._execute_active_peripheral_request( utterance, provider=provider, conversation_id=conversation_id, execution_mode=execution_mode, on_commands_validated=publish_peripheral_validation, natural_language_routing=natural_language_routing, ) if result is not None: if not peripheral_validation_published: publish_peripheral_validation(result) if peripheral_validation_elapsed_ms is not None: result.update( { "local_control_budget_ms": LOCAL_CONTROL_BUDGET_MS, "local_control_elapsed_ms": ( peripheral_validation_elapsed_ms ), "local_control_within_budget": ( peripheral_validation_elapsed_ms < LOCAL_CONTROL_BUDGET_MS ), "natural_language_routing": deepcopy( natural_language_routing ), } ) return result # Compile, publish and execute every deterministic IR action before # any hosted planner is contacted. if entries: peripheral_validation_published = False infrared_result = self._execute_local_peripheral_actions( utterance, entries, provider=provider, conversation_id=conversation_id, execution_mode=execution_mode, on_commands_validated=publish_peripheral_validation, ) if not peripheral_validation_published: publish_peripheral_validation(infrared_result) if peripheral_validation_elapsed_ms is not None: infrared_result.update( { "local_control_budget_ms": LOCAL_CONTROL_BUDGET_MS, "local_control_elapsed_ms": ( peripheral_validation_elapsed_ms ), "local_control_within_budget": ( peripheral_validation_elapsed_ms < LOCAL_CONTROL_BUDGET_MS ), "natural_language_routing": deepcopy( natural_language_routing ), } ) # The native preview is also strictly local. Its own manifest records # unresolved CAN clauses for the later supplement endpoint. if residual: can_arguments = { **common_arguments, "utterance": residual, "_cloud_instruction_context": natural_language_routing, } can_validation_callback = callback if callback is not None and infrared_result is not None: def publish_combined_validation( validated_can: dict[str, object], ) -> None: validated = self._merge_can_and_peripheral_results( utterance, validated_can, infrared_result, entries, ) validated = self._apply_nfc_command_denials(validated, denied) validated["status"] = "processing" validated["phase"] = "commands_validated" callback(validated) can_validation_callback = publish_combined_validation if path == "/api/requests/preview": can_result = self.service.preview_native_can( **can_arguments, on_commands_validated=can_validation_callback, ) else: can_result = self.service.process( **can_arguments, _on_commands_validated=can_validation_callback, ) routed = can_result.get("natural_language_routing") if isinstance(routed, dict): natural_language_routing = deepcopy(routed) # Hosted IR planning is deliberately last. Only the unresolved IR # text is submitted, while the trusted context retains the full # sentence and the clauses already handled above. if unresolved and provider != "local": try: candidates = list(partition.get("unresolved_candidates") or []) if not candidates: raise ExtensionIntegrationError( "没有可供云端补充选择的已学习红外能力。" ) cloud_utterance = ",".join(unresolved_peripheral_clauses) plan_result = self.extension.plan( { "utterance": cloud_utterance, "peripherals": candidates, "vehicle_devices": [], "provider": provider, "natural_language_routing": { **natural_language_routing, "cloud_input_utterance": cloud_utterance, }, } ) if plan_result.get("needs_confirmation"): raise ExtensionIntegrationError( str( plan_result.get("question") or "请明确要控制的红外动作。" ) ) cloud_steps = list(plan_result.get("steps") or []) if not cloud_steps: raise ExtensionIntegrationError( "云端模型没有命中已学习红外能力。" ) candidate_by_id = { str(item.get("id")): { **item, "parse_source": "cloud_model", } for item in candidates if isinstance(item, dict) and item.get("id") } selected_cloud = [ candidate_by_id[str(step.get("capability_id"))] for step in cloud_steps if isinstance(step, dict) and step.get("kind") == "peripheral" and str(step.get("capability_id")) in candidate_by_id ] if not selected_cloud: raise ExtensionIntegrationError( "云端模型没有返回允许执行的已学习红外能力。" ) conversation_turn = 1 if conversation_id: context = self.service.conversations.snapshot(conversation_id) conversation_turn = int(context.get("turn_count", 0)) + 1 cloud_infrared_result = ( self.extension.execute_peripheral_plan_request( utterance, plan_result, selected_cloud, conversation_id=conversation_id, conversation_turn=conversation_turn, execution_mode=execution_mode, provider_requested=provider, ) ) next_index = max( [ int(entry.get("index", 0)) for entry in entries if isinstance(entry, dict) ], default=-1, ) + 1 entries.extend( { "index": next_index + index, "clause": cloud_utterance, "capability": capability, } for index, capability in enumerate(selected_cloud) ) infrared_result = ( self._merge_peripheral_phase_results( utterance, infrared_result, cloud_infrared_result, ) if infrared_result is not None else cloud_infrared_result ) except ExtensionIntegrationError as exc: cloud_supplement_error = str(exc) if infrared_result is None and can_result is None: raise if can_result is not None and infrared_result is not None: result = self._merge_can_and_peripheral_results( utterance, can_result, infrared_result, entries, ) elif can_result is not None: result = can_result result["utterance"] = utterance elif infrared_result is not None: result = infrared_result elif unresolved and provider == "local": local_elapsed_ms = round( (time.perf_counter() - local_stage_started) * 1000, 3, ) request_id = f"req-local-preview-{os.urandom(8).hex()}" result = { "request_id": request_id, "preview_request_id": request_id, "utterance": utterance, "conversation_id": conversation_id, "status": "no_action", "success": False, "phase": "deterministic_preview", "provider_requested": provider, "provider_used": "local_rules", "execution_mode": execution_mode, "decision_summary": [ "本地规则未生成控制;仅未解析子句等待云端补充。" ], "function_config": None, "function_configs": {}, "command_plan": None, "command_plans": {}, "trace_events": [], "validation": [], "execution_report": None, "operation_results": [], "parsed_control_signatures": [], "preview_elapsed_ms": local_elapsed_ms, "local_control_budget_ms": LOCAL_CONTROL_BUDGET_MS, "local_control_elapsed_ms": local_elapsed_ms, "local_control_within_budget": ( local_elapsed_ms < LOCAL_CONTROL_BUDGET_MS ), "supplement_required": True, } else: local_elapsed_ms = round( (time.perf_counter() - local_stage_started) * 1000, 3, ) request_id = f"req-local-blocked-{os.urandom(8).hex()}" result = { "request_id": request_id, "preview_request_id": request_id, "utterance": utterance, "conversation_id": conversation_id, "status": "blocked", "success": False, "outcome": "failed_or_blocked", "phase": "authorization_blocked", "orchestration_route": "local_peripheral_authorization", "provider_requested": provider, "provider_used": "infrared_local_rules", "execution_mode": execution_mode, "control_transport": "gscu_tcp", "decision_summary": [], "function_config": None, "function_configs": {}, "command_plan": None, "command_plans": {}, "trace_events": [], "validation": [], "execution_report": None, "operation_results": [], "parsed_control_signatures": [], "preview_elapsed_ms": local_elapsed_ms, "local_control_budget_ms": LOCAL_CONTROL_BUDGET_MS, "local_control_elapsed_ms": local_elapsed_ms, "local_control_within_budget": ( local_elapsed_ms < LOCAL_CONTROL_BUDGET_MS ), "supplement_required": False, } result["natural_language_routing"] = deepcopy(natural_language_routing) for key in ("parsed_clauses", "resolved_clauses", "unresolved_clauses"): value = natural_language_routing.get(key) if isinstance(value, list): result[key] = deepcopy(value) if cloud_supplement_error: result["cloud_supplement_error"] = { "stage": "unresolved_peripheral_clauses", "message": cloud_supplement_error, } result["status"] = "partially_succeeded" result["success"] = False result["decision_summary"] = [ *list(result.get("decision_summary") or []), "明确控制已由本地规则先行处理;未解析的红外子句云端补充失败。", ] result = self._apply_nfc_command_denials(result, denied) self.service.persist_composed_result(result) if callback is not None: callback(result) return result def _stream_control_request( self, path: str, supplement_match: re.Match[str] | None, payload: dict[str, object], common_arguments: dict[str, object], *, source: str, ) -> None: """Stream validated command plans before physical execution finishes.""" sync_vehicle_task = source not in NON_DISPATCH_DISPLAY_SOURCES self.send_response(HTTPStatus.OK) self.send_header("Content-Type", "application/x-ndjson; charset=utf-8") self.send_header("Cache-Control", "no-store") self.send_header("X-Accel-Buffering", "no") self._send_security_headers() self.end_headers() stream_open = True def emit(event: dict[str, object]) -> None: nonlocal stream_open if not stream_open: return content = (json.dumps(event, ensure_ascii=False) + "\n").encode( "utf-8" ) try: self.wfile.write(content) self.wfile.flush() except (BrokenPipeError, ConnectionResetError): # The submitted control request must continue even if the # browser closes while the physical execution is in progress. stream_open = False def on_commands_validated(result: dict[str, object]) -> None: emit({"type": "validated_commands", "payload": result}) def emit_terminal_error( code: str, message: str, *, detail: str | None = None, extra: dict[str, object] | None = None, ) -> None: """Publish one terminal failure using can_read_trans outcome terms.""" error = { "code": code, "message": message, "outcome": "failed_or_blocked", **({"detail": detail} if detail else {}), } if sync_vehicle_task: self.vehicle_display_sync.update_task( status="failed", utterance=utterance, source=source, error=error, ) emit({ "type": "error", "error": code, "message": message, "outcome": "failed_or_blocked", **({"detail": detail} if detail else {}), **(extra or {}), }) utterance = str(common_arguments.get("utterance") or "") try: result = None if path in {"/api/requests", "/api/requests/preview"}: result = self._execute_request_with_peripherals( path, common_arguments, on_commands_validated=on_commands_validated, ) if result is None: if path == "/api/requests/preview": result = self.service.preview_native_can( **common_arguments, on_commands_validated=on_commands_validated, ) elif supplement_match: result = self.service.supplement_native_can( supplement_match.group("request_id"), provider=( str(payload["provider"]) if payload.get("provider") else None ), approval_token=os.getenv( "COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN" ), hardware_client_id=( str(payload["hardware_client_id"]) if payload.get("hardware_client_id") else None ), hardware_access_bypass=self._hardware_access_bypass_allowed(), reasoning_effort=( str(payload["reasoning_effort"]) if payload.get("reasoning_effort") is not None else None ), on_commands_validated=on_commands_validated, ) else: result = self.service.process( **common_arguments, _on_commands_validated=on_commands_validated, ) utterance = str(result.get("utterance") or utterance) if sync_vehicle_task: self.vehicle_display_sync.update_task( status=str(result.get("status", "succeeded")), utterance=utterance, source=source, result=result, ) emit({"type": "result", "payload": result}) except HardwareAccessRequiredError as exc: emit_terminal_error( "hardware_access_required", str(exc), extra={"access_state": exc.state}, ) except HardwareBusyError as exc: emit_terminal_error( "hardware_busy", str(exc), extra={"retry_after_seconds": exc.retry_after_seconds}, ) except HardwareSafetyError as exc: emit_terminal_error("hardware_safety_gate", str(exc)) except CanGatewayError as exc: emit_terminal_error("can_transport_failed", str(exc)) except GatewayError as exc: emit_terminal_error("gscu_unavailable", str(exc)) except ExtensionIntegrationError as exc: emit_terminal_error("extension_request_ambiguous", str(exc)) except (ValueError, json.JSONDecodeError) as exc: emit_terminal_error("invalid_request", str(exc)) except OpenAIIntentError as exc: self.log_error("OpenAI intent request failed: %s", exc) emit_terminal_error( "openai_request_failed", "GPT 意图解析失败,请检查 API Key、模型权限或网络状态。", detail=str(exc), ) except Exception as exc: # noqa: BLE001 - stream a terminal event self.log_error("streaming request failed: %s", exc) emit_terminal_error( "internal_error", "请求处理失败,请查看服务端日志。", detail=str(exc), ) def _assistant_capability_catalog(self) -> AssistantCapabilityCatalog: """Bind the assistant to the same live registries used by every service.""" if self.assistant_capability_catalog is not None: return self.assistant_capability_catalog vehicle_service = ( self.service if hasattr(self.service, "capability_registry") else SeatDemoHandler.service ) return AssistantCapabilityCatalog( vehicle_service, peripheral_provider=self.extension.list_peripheral_capabilities, nfc_provider=self.nfc.snapshot, speech_provider=self.speech.status, gesture_provider=self.gesture.runtime_info, calibration_provider=self.calibration.snapshot, self_repair_provider=self.repair.health, ) def _handle_assistant_stream(self) -> None: """Serve one unified answer/control turn as incremental NDJSON.""" try: username = self._session_username() principal = username or f"anonymous:{self._client_key()}" retry_after = self.auth.api_requests.retry_after(principal) if retry_after: self._json( {"error": "rate_limited", "message": "请求过于频繁,请稍后再试。"}, HTTPStatus.TOO_MANY_REQUESTS, {"Retry-After": str(retry_after)}, ) return self.auth.api_requests.record(principal) payload = self._read_json_body(max_bytes=64 * 1024) except (ValueError, json.JSONDecodeError) as exc: self._json( {"error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST, ) return message = str(payload.get("message") or payload.get("utterance") or "").strip() conversation_id = str(payload.get("conversation_id") or "").strip() if not conversation_id: conversation_id = f"conv-{os.urandom(16).hex()}" history_payload = payload.get("history") history = history_payload if isinstance(history_payload, list) else [] source = str(payload.get("client_type") or "web_console_assistant") hardware_client_id = ( str(payload["hardware_client_id"]) if payload.get("hardware_client_id") else None ) reasoning_effort = ( str(payload["reasoning_effort"]) if payload.get("reasoning_effort") is not None else "low" ) mode_profile_draft_context = payload.get("mode_profile_draft_context") parsed_mode_update = parse_mode_profile_update( message, expected_mode_name=( mode_profile_draft_context.get("name") if isinstance(mode_profile_draft_context, Mapping) else None ), ) matching_mode_profile_draft_context = ( dict(mode_profile_draft_context) if isinstance(mode_profile_draft_context, Mapping) and parsed_mode_update is not None and normalize_mode_profile_name(parsed_mode_update[0]) == normalize_mode_profile_name( mode_profile_draft_context.get("name") ) else None ) requested_vehicle_id = ( str(payload["vehicle_id"]) if payload.get("vehicle_id") else None ) common_arguments: dict[str, object] = { "utterance": message, # Explicit controls take the deterministic preview path first. # ``auto`` permits a second phase only when that preview reports # genuinely unresolved clauses; fully local requests never invoke # a hosted model. "provider": "auto", "conversation_id": conversation_id, "execution_mode": str(payload.get("execution_mode") or "dry_run"), "approval_token": os.getenv("COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"), "vehicle_id": requested_vehicle_id, "simulation_fault": str(payload.get("simulation_fault") or "normal"), "hardware_client_id": hardware_client_id, "hardware_access_bypass": self._hardware_access_bypass_allowed(), "reasoning_effort": reasoning_effort, "control_transport": str(payload.get("control_transport") or "wired_can"), **( { "mode_profile_draft_context": ( matching_mode_profile_draft_context ) } if matching_mode_profile_draft_context is not None else {} ), } capability_catalog = self._assistant_capability_catalog() try: capability_snapshot = capability_catalog.snapshot( vehicle_id=requested_vehicle_id ) except ValueError as exc: self._json( {"error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST, ) return capability_context = capability_catalog.compact_context(capability_snapshot) scene_capability_context = capability_catalog.scene_composition_context( capability_snapshot ) fast_answer = capability_catalog.fast_answer(message, capability_snapshot) quick_control_provider = getattr(capability_catalog, "quick_controls", None) quick_controls = ( quick_control_provider(capability_snapshot) if callable(quick_control_provider) else () ) self.send_response(HTTPStatus.OK) self.send_header("Content-Type", "application/x-ndjson; charset=utf-8") self.send_header("Cache-Control", "no-store") self.send_header("X-Accel-Buffering", "no") self._send_security_headers() self.end_headers() stream_open = True def emit(event: dict[str, object]) -> bool: nonlocal stream_open if not stream_open: return False content = (json.dumps(event, ensure_ascii=False) + "\n").encode("utf-8") try: self.wfile.write(content) self.wfile.flush() return True except (BrokenPipeError, ConnectionResetError): stream_open = False return False def record_control_failure(code: str, public_message: str) -> None: self.vehicle_display_sync.update_task( status="failed", utterance=message, source=source, error={"code": code, "message": public_message}, ) local_preview_result: dict[str, object] | None = None def preview_control( instruction: str, on_commands_validated: Callable[[dict[str, object]], None], ) -> dict[str, object]: """Run only deterministic phases and retain their merge evidence.""" nonlocal local_preview_result self.vehicle_display_sync.update_task( status="running", utterance=message, source=source, ) preview_arguments = { **common_arguments, "utterance": instruction, "provider": "local", } result = self._execute_request_with_peripherals( "/api/requests/preview", preview_arguments, on_commands_validated=on_commands_validated, ) if result is None: result = self.service.preview_native_can( **preview_arguments, on_commands_validated=on_commands_validated, ) local_preview_result = result self.vehicle_display_sync.update_task( status=str(result.get("status") or "succeeded"), utterance=message, source=source, result=result, ) return result def supplement_control( instruction: str, on_commands_validated: Callable[[dict[str, object]], None], assistant_plan: Mapping[str, object] | None = None, ) -> dict[str, object]: """Execute a Codex proposal for unresolved text without replay.""" preview = local_preview_result if preview is None: raise CodexAssistantError("local control preview is missing") routing_value = preview.get("natural_language_routing") routing = ( deepcopy(routing_value) if isinstance(routing_value, Mapping) else { "original_utterance": message, "parsed_clauses": [], "resolved_clauses": [], "unresolved_clauses": [instruction], } ) routing["cloud_input_utterance"] = instruction excluded_signatures = { tuple(item) for item in preview.get("parsed_control_signatures", []) if isinstance(item, list) } def publish_supplement(validated: dict[str, object]) -> None: merged_event = self.service._merge_native_can_phases( preview, validated, ) merged_event.update( { "phase": "supplement_commands_validated", "preview_request_id": preview.get("request_id"), "supplement_of": preview.get("request_id"), "natural_language_routing": deepcopy(routing), } ) on_commands_validated(merged_event) process_arguments = { key: value for key, value in common_arguments.items() if key not in { "utterance", "provider", "mode_profile_draft_context", } } if assistant_plan is not None: supplement = self.service.process( instruction, provider="local", **process_arguments, _excluded_control_signatures=excluded_signatures, _model_parse_source="codex_supplement", _allow_empty_control_result=True, _on_commands_validated=publish_supplement, _assistant_plan=dict(assistant_plan), _cloud_instruction_context=routing, ) else: supplement_arguments = { **common_arguments, "utterance": instruction, "provider": "local", } supplement = self._execute_request_with_peripherals( "/api/requests", supplement_arguments, on_commands_validated=publish_supplement, ) if supplement is None: supplement = self.service.process( instruction, provider="local", **process_arguments, _excluded_control_signatures=excluded_signatures, _model_parse_source="codex_supplement", _allow_empty_control_result=True, _on_commands_validated=publish_supplement, _cloud_instruction_context=routing, ) merged = self.service._merge_native_can_phases(preview, supplement) merged.update( { "phase": "two_stage_complete", "utterance": message, "preview_request_id": preview.get("request_id"), "supplement_of": preview.get("request_id"), "supplement_request_id": supplement.get("request_id"), "supplement_required": False, "natural_language_routing": routing, } ) self.service.persist_composed_result(merged) self.vehicle_display_sync.update_task( status=str(merged.get("status") or "succeeded"), utterance=message, source=source, result=merged, ) return merged def execute_control( instruction: str, on_commands_validated: Callable[[dict[str, object]], None], assistant_plan: Mapping[str, object] | None = None, ) -> dict[str, object]: self.vehicle_display_sync.update_task( status="running", utterance=message, source=source, ) turn_arguments = { **common_arguments, "utterance": instruction, # Codex has already normalized this agent-tool request. Keep # the safety execution pass deterministic; explicit user text # uses preview_control/supplement_control above instead. "provider": "local", **({"assistant_plan": dict(assistant_plan)} if assistant_plan is not None else {}), } try: result = self._execute_request_with_peripherals( "/api/requests/preview", turn_arguments, on_commands_validated=on_commands_validated, ) if result is None: result = self.service.preview_native_can( **turn_arguments, on_commands_validated=on_commands_validated, ) if result.get("supplement_required"): result = self.service.supplement_native_can( str(result["request_id"]), provider="local", approval_token=os.getenv("COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"), hardware_client_id=hardware_client_id, hardware_access_bypass=self._hardware_access_bypass_allowed(), reasoning_effort=reasoning_effort, on_commands_validated=on_commands_validated, ) except HardwareAccessRequiredError: record_control_failure("hardware_access_required", "真实车控当前不可用。") raise except HardwareBusyError: record_control_failure("hardware_busy", "车辆控制通道正在使用中。") raise except HardwareSafetyError: record_control_failure("hardware_safety_gate", "操作未通过车辆安全校验。") raise except CanGatewayError: record_control_failure("can_transport_failed", "车辆控制通道当前不可用。") raise except (GatewayError, ExtensionIntegrationError): record_control_failure("device_control_failed", "外设控制没有完成。") raise except Exception: record_control_failure("vehicle_control_failed", "车辆控制没有完成。") raise self.vehicle_display_sync.update_task( status=str(result.get("status") or "succeeded"), utterance=message, source=source, result=result, ) return result try: self.assistant.stream( message, conversation_id=conversation_id, principal=principal, history=history, emit=emit, execute_control=execute_control, preview_control=preview_control, supplement_control=supplement_control, capability_snapshot=capability_snapshot, capability_context=capability_context, scene_capability_context=scene_capability_context, control_terms=capability_catalog.control_terms(capability_snapshot), query_capabilities=capability_catalog.query, authoritative_answer=fast_answer.answer if fast_answer else None, authoritative_topic=( getattr(fast_answer, "topic", None) if fast_answer else None ), authoritative_suggestions=( [ item.as_dict() for item in getattr(fast_answer, "suggestions", ()) ] if fast_answer else [] ), quick_control_commands=[item.command for item in quick_controls], mode_profile_draft_context=mode_profile_draft_context, ) except HardwareAccessRequiredError as exc: self.vehicle_display_sync.update_task( status="failed", utterance=message, source=source, error={"code": "hardware_access_required", "message": str(exc)}, ) emit( { "type": "error", "error": "hardware_access_required", "message": f"暂时不能执行真实车控:{exc}", "access_state": exc.state, } ) except HardwareBusyError as exc: record_control_failure("hardware_busy", "车辆控制通道正在使用中。") emit( { "type": "error", "error": "hardware_busy", "message": f"车辆控制通道正在使用中:{exc}", "retry_after_seconds": exc.retry_after_seconds, } ) except HardwareSafetyError as exc: record_control_failure("hardware_safety_gate", "操作未通过车辆安全校验。") emit( { "type": "error", "error": "hardware_safety_gate", "message": f"该操作未通过车辆安全校验:{exc}", } ) except CanGatewayError as exc: record_control_failure("can_transport_failed", "车辆控制通道当前不可用。") emit( { "type": "error", "error": "can_transport_failed", "message": f"车辆控制通道当前不可用:{exc}", } ) except (GatewayError, ExtensionIntegrationError) as exc: record_control_failure("device_control_failed", "外设控制没有完成。") emit( { "type": "error", "error": "device_control_failed", "message": f"外设控制没有完成:{exc}", } ) except (CodexAppServerError, CodexAssistantError, OpenAIIntentError) as exc: self.log_error("assistant model request failed: %s", exc) emit( { "type": "error", "error": "assistant_model_failed", "message": "小迪暂时没有响应,请稍后再试。", } ) except (ValueError, json.JSONDecodeError) as exc: emit({"type": "error", "error": "invalid_request", "message": str(exc)}) except Exception as exc: # noqa: BLE001 - stream one safe terminal event self.log_error("assistant request failed: %s", exc) emit( { "type": "error", "error": "internal_error", "message": "请求处理失败,请稍后再试。", } ) def _handle_codex_vehicle_control(self) -> None: """Run an allowlisted MCP tool over the authenticated loopback bridge.""" try: content_length = int(self.headers.get("Content-Length", "0")) except ValueError: content_length = 0 if not self._is_unproxied_loopback_request(): self._json( {"error": "local_access_required", "message": "仅允许本机 Codex 工具调用。"}, HTTPStatus.FORBIDDEN, ) self._drain_rejected_request_body(content_length, max_bytes=32 * 1024) return if not self.assistant.authenticate_internal_request( self.headers.get("X-N7-Internal-Token", "") ): self._json( {"error": "invalid_internal_token", "message": "Codex 工具认证失败。"}, HTTPStatus.FORBIDDEN, ) self._drain_rejected_request_body(content_length, max_bytes=32 * 1024) return try: payload = self._read_json_body(max_bytes=32 * 1024) tool_name = str(payload.get("tool") or "execute_cockpit_request") request_token = str(payload.get("request_token") or "") if tool_name == "get_n7web_capabilities": result = self.assistant.get_n7web_capabilities( request_token, str(payload.get("query") or ""), ) elif tool_name == "execute_cockpit_request": plan = payload.get("plan") if plan is not None and not isinstance(plan, dict): raise ValueError("plan must be an object") result = self.assistant.execute_cockpit_request( request_token, plan, ) else: raise ValueError("unsupported Codex application tool") self._json({"success": True, "result": result}) except (ValueError, json.JSONDecodeError) as exc: self._json( {"error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST, ) except CodexVehicleToolError as exc: self._json( {"error": "vehicle_control_failed", "message": str(exc)}, HTTPStatus.CONFLICT, ) except Exception as exc: # noqa: BLE001 - return a safe MCP failure self.log_error("Codex vehicle tool failed: %s", exc) self._json( {"error": "vehicle_control_failed", "message": str(exc)}, HTTPStatus.CONFLICT, ) def do_PUT(self) -> None: # noqa: N802 - stdlib handler API path = unquote(urlparse(self.path).path) if not self._authorize(path): return if not self._require_same_origin() or not self._require_json_content_type(): return pet_match = re.fullmatch( r"/api/extension/pets/(?Ppet-[0-9a-f]{32})", path, ) if pet_match is not None: try: payload = self._read_json_body(max_bytes=4096) unexpected = set(payload) - {"pet_name", "action"} if unexpected: if "nfc_id" in unexpected: raise ValueError( "nfc_id 只能通过当前已识别的 NFC 卡片完成绑定。" ) raise ValueError("宠物卡片包含不支持的字段。") card = self.pets.update_card( pet_match.group("pet_id"), pet_name=payload.get("pet_name"), action=payload.get("action"), ) self._json( {"success": True, "card": card, **self.pets.snapshot()}, HTTPStatus.OK, ) except KeyError: self._json( {"success": False, "error": "not_found", "message": "未找到宠物卡片。"}, HTTPStatus.NOT_FOUND, ) except (ValueError, json.JSONDecodeError) as exc: self._json( {"success": False, "error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST, ) return match = re.fullmatch(r"/api/extension/gscu/slots/(?P\d+)", path) if match is None: self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND) return try: payload = self._read_json_body(max_bytes=4096) self._json( self.extension.save_infrared_slot(match.group("slot"), payload), HTTPStatus.OK, ) except (GatewayError, ValueError, json.JSONDecodeError) as exc: self._json( {"success": False, "error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST, ) def do_DELETE(self) -> None: # noqa: N802 - stdlib handler API parsed_url = urlparse(self.path) path = unquote(parsed_url.path) if not self._authorize(path): return if not self._require_same_origin(): return slot_match = re.fullmatch(r"/api/extension/gscu/slots/(?P\d+)", path) if slot_match is not None: try: self._json( self.extension.delete_infrared_slot(slot_match.group("slot")), HTTPStatus.OK, ) except GatewayError as exc: self._json( {"success": False, "error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST, ) return pet_match = re.fullmatch( r"/api/extension/pets/(?Ppet-[0-9a-f]{32})", path, ) if pet_match is not None: try: deleted = self.pets.delete_card(pet_match.group("pet_id")) self._json( {"success": True, "deleted": deleted, **self.pets.snapshot()}, HTTPStatus.OK, ) except KeyError: self._json( {"success": False, "error": "not_found", "message": "未找到宠物卡片。"}, HTTPStatus.NOT_FOUND, ) except ValueError as exc: self._json( {"success": False, "error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST, ) return prefix = "/api/mode-profiles/" if not path.startswith(prefix): self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND) return mode_id = path.removeprefix(prefix).strip() vehicle_id = str(parse_qs(parsed_url.query).get("vehicle_id", [""])[0]) or None try: self._json( self.service.delete_user_mode_profile(mode_id, vehicle_id), HTTPStatus.OK, ) except KeyError: self._json( {"error": "not_found", "message": "未找到可删除的用户自定义模式。"}, HTTPStatus.NOT_FOUND, ) except PermissionError as exc: self._json( {"error": "preset_mode_protected", "message": str(exc)}, HTTPStatus.FORBIDDEN, ) except ValueError as exc: self._json( {"error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST, ) def _handle_mode_profile_post(self, path: str) -> None: execute_match = re.fullmatch( r"/api/mode-profiles/(?P[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 create_mode = path == "/api/mode-profiles" mode_id = path.removeprefix("/api/mode-profiles/").strip() if not create_mode and not mode_id: self._json( {"error": "invalid_request", "message": "模式编号不能为空。"}, HTTPStatus.BAD_REQUEST, ) return try: payload = self._read_json_body(max_bytes=16 * 1024) arguments = ( str(payload.get("name", "")), str(payload.get("instruction", "")), str(payload.get("source_request_id", "")), str(payload.get("vehicle_id", "")) or None, ) result = ( self.service.create_mode_profile_card(*arguments) if create_mode else self.service.save_mode_profile_card(mode_id, *arguments) ) saved_profile = result.get("mode_profile") or {} saved_mode_id = str(saved_profile.get("id") or "").strip() try: result["mode_scene_mqtt"] = self.service.publish_saved_mode_scene( saved_mode_id, str(result.get("vehicle_id") or "") or None, ) except Exception as exc: # noqa: BLE001 - card persistence must survive MQTT loss result["mode_scene_mqtt"] = { "state": "queue_failed", "topic": MQTT_MODE_SCENE_TOPIC, "scene_id": saved_mode_id or None, "burst_count": MODE_SCENE_MQTT_BURST_COUNT, "burst_window_ms": round( MODE_SCENE_MQTT_BURST_WINDOW_S * 1_000 ), "last_error": str(exc)[:240], } self._json(result, HTTPStatus.CREATED if create_mode else HTTPStatus.OK) except KeyError: self._json( {"error": "not_found", "message": "未找到需要编辑的模式卡片。"}, HTTPStatus.NOT_FOUND, ) except (ValueError, json.JSONDecodeError) as exc: self._json( {"error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST, ) def _handle_mode_profile_draft_trial(self) -> None: try: payload = self._read_json_body(max_bytes=16 * 1024) result = self.service.trial_mode_profile_draft( str(payload.get("name", "")), str(payload.get("instruction", "")), str(payload.get("source_request_id", "")), str(payload.get("vehicle_id", "")) or None, ) self._json(result, HTTPStatus.OK) except (ValueError, json.JSONDecodeError) as exc: self._json( {"error": "invalid_request", "message": str(exc)}, 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 [] 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() bound_uid = re.sub( r"[^0-9A-Fa-f]", "", str(target.get("card_uid") or ""), ).upper() if not bound_uid: raise ExtensionIntegrationError( f"已保存模式中的红外命令缺少持久 NFC 绑定:{device_id} / IR {slot}。" ) 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() == bound_uid ), None, ) if capability is None: raise ExtensionIntegrationError( f"持久绑定目录未授权模式中的红外命令:{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 against the durable 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._hardware_access_bypass_allowed(), 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: action_map = { "/api/extension/gscu/info": "info", "/api/extension/gscu/learn": "learn", "/api/extension/gscu/cancel": "cancel", "/api/extension/gscu/emit": "emit", "/api/extension/gscu/read": "read", "/api/extension/gscu/write": "write", "/api/extension/gscu/restart": "restart", "/api/extension/gscu/reset": "reset", "/api/extension/gscu/wifi-lock": "wifi_lock", "/api/extension/gscu/raw": "raw", } try: payload = self._read_json_body(max_bytes=256 * 1024) if path == "/api/extension/pets": if "nfc_id" in payload: raise ValueError( "nfc_id 只能通过当前已识别的 NFC 卡片完成绑定。" ) card = self.pets.create_card( payload.get("pet_name"), payload.get("action"), ) self._json( {"success": True, "card": card, **self.pets.snapshot()}, HTTPStatus.CREATED, ) return pet_binding_match = re.fullmatch( r"/api/extension/pets/(?Ppet-[0-9a-f]{32})/" r"(?Pbind-current-nfc|unbind)", path, ) if pet_binding_match is not None: pet_id = pet_binding_match.group("pet_id") try: if pet_binding_match.group("action") == "bind-current-nfc": card = self.pets.bind_current_nfc(pet_id) else: card = self.pets.unbind_card(pet_id) except KeyError: self._json( { "success": False, "error": "not_found", "message": "未找到宠物卡片。", }, HTTPStatus.NOT_FOUND, ) return self._json( {"success": True, "card": card, **self.pets.snapshot()}, HTTPStatus.OK, ) return if path in { "/api/extension/gscu/erase", "/api/extension/gscu/erase-all", }: result = self.extension.erase_all_infrared(dict(payload)) self.vehicle_display_sync.update_extension( action={"type": "erase_all"}, result=result, ) self._json(result) return action = action_map.get(path) if action: self._handle_extension_gateway(action, payload) return if path == "/api/extension/gscu/config": result = self.extension.configure_tcp_gateway(dict(payload)) self.vehicle_display_sync.update_extension( action={ "type": "gateway_config", "listen_host": result.get("listen_host"), "port": result.get("port"), }, result=result, ) self._json(result) return if path == "/api/extension/display-sync": catalog = payload.get("peripherals", payload.get("catalog", [])) if not isinstance(catalog, list) or len(catalog) > 248: raise ValueError("外设能力目录必须是至多 248 项的数组。") durable_sync = self.extension.sync_peripheral_catalog(catalog) sync_result = { "status": "ready", "count": len(catalog), "learned_count": durable_sync["count"], } self.vehicle_display_sync.update_extension( catalog=catalog, action={ "type": "hardware_detected", "device": payload.get("device"), }, result=sync_result, ) self._json({"success": True, **sync_result}) return if path == "/api/extension/assistant/plan": self.vehicle_display_sync.update_extension( catalog=payload.get("peripherals", payload.get("catalog", [])), action={"type": "plan", "command": payload.get("utterance", "")}, ) result = self.extension.plan(payload) self.vehicle_display_sync.update_extension( result=result, ) self._json(result) return if path == "/api/extension/vehicle/execute": result = self.extension.execute_vehicle( payload, self.service, hardware_access_bypass=self._hardware_access_bypass_allowed(), ) self.vehicle_display_sync.update_task( status=str(result.get("status", "succeeded")), utterance=str(payload.get("command", payload.get("utterance", ""))), source=str(payload.get("client_type", "head_unit")), result=result, ) self.vehicle_display_sync.update_extension( catalog=payload.get("catalog") if "catalog" in payload else None, action={"type": "vehicle_execute"}, result=result, ) self._json(result) return self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND) except GatewayError as exc: self._json( {"success": False, "error": "gscu_unavailable", "message": str(exc)}, HTTPStatus.SERVICE_UNAVAILABLE, ) except ExtensionIntegrationError as exc: self._json( {"success": False, "error": "extension_unavailable", "message": str(exc)}, HTTPStatus.SERVICE_UNAVAILABLE, ) except HardwareAccessRequiredError as exc: self._json( { "success": False, "error": "hardware_access_required", "message": str(exc), "access_state": exc.state, }, HTTPStatus.FORBIDDEN, ) except HardwareBusyError as exc: self._json( { "success": False, "error": "hardware_busy", "message": str(exc), "retry_after_seconds": exc.retry_after_seconds, }, HTTPStatus.CONFLICT, {"Retry-After": str(exc.retry_after_seconds)}, ) except HardwareSafetyError as exc: self._json( {"success": False, "error": "hardware_safety_gate", "message": str(exc)}, HTTPStatus.FORBIDDEN, ) except OpenAIIntentError as exc: self.log_error("extension vehicle intent request failed: %s", exc) self._json( { "success": False, "error": "openai_request_failed", "message": "车端 GPT 意图解析失败,请检查模型配置。", }, HTTPStatus.BAD_GATEWAY, ) except (ValueError, json.JSONDecodeError) as exc: self._json( {"success": False, "error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST, ) except Exception as exc: # noqa: BLE001 - keep extension APIs responsive self.log_error("self-extension request failed: %s", exc) self._json( { "success": False, "error": "internal_error", "message": "自扩展服务处理失败,请查看服务端日志。", }, HTTPStatus.INTERNAL_SERVER_ERROR, ) def _handle_calibration_post(self, path: str) -> None: if not self._require_calibration_admin(): return try: payload = self._read_json_body(max_bytes=256 * 1024) action_map = { "/api/calibration/can/connect": lambda: self.calibration.connect_can( dict(payload.get("can", payload)) ), "/api/calibration/start": lambda: self.calibration.calibrate( None if payload.get("axis", "all") == "all" else int(payload["axis"]) ), "/api/calibration/move": lambda: self.calibration.move( int(payload["axis"]), int(payload["permille"]) ), "/api/calibration/seek": lambda: self.calibration.seek( int(payload["axis"]), bool(payload.get("far")) ), "/api/calibration/stop": self.calibration.stop, "/api/calibration/session/reset": self.calibration.reset_virtual_session, "/api/calibration/settings": lambda: self.calibration.update_settings( dict(payload) ), "/api/calibration/mqtt/connect": self.calibration.connect_mqtt, "/api/calibration/mqtt/disconnect": self.calibration.disconnect_mqtt, "/api/calibration/mqtt/test": self.calibration.mqtt_test, "/api/calibration/video/start": self.calibration.start_video, "/api/calibration/video/stop": self.calibration.stop_video, } action = action_map.get(path) if action is None: self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND) return requested_mode = str( dict(payload.get("can", {})).get( "mode", self.calibration.snapshot()["transport"]["mode"] ) ) motion_paths = { "/api/calibration/can/connect", "/api/calibration/start", "/api/calibration/move", "/api/calibration/seek", "/api/calibration/stop", } if path in motion_paths and requested_mode == "hardware": if not self._require_local_decision_point(): return self._json({"ok": True, "state": action()}) except CalibrationApiError as exc: self._json( {"ok": False, "error": "calibration_error", "message": str(exc)}, HTTPStatus(exc.status), ) except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc: self._json( {"ok": False, "error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST, ) except Exception as exc: # noqa: BLE001 - keep calibration APIs responsive self.log_error("self-calibration request failed: %s", exc) self._json( { "ok": False, "error": "internal_error", "message": "自标定服务处理失败,请查看服务端日志。", }, HTTPStatus.INTERNAL_SERVER_ERROR, ) def _handle_self_repair_post(self, path: str) -> None: if not self._require_self_repair_admin(): return try: payload = self._read_json_body(max_bytes=256 * 1024) physical_paths = { "/api/self-repair/hardware/start", "/api/self-repair/hardware/stop", "/api/self-repair/emergency-stop", "/api/self-repair/firmware/build-flash", "/api/self-repair/open-log", } if ( path in physical_paths or ( path in { "/api/self-repair/runtime", "/api/self-repair/healing/start", "/api/self-repair/healing/reset", } and str(payload.get("execution_mode", "shadow")) == "bench" ) ) and not self._require_local_decision_point(): return if path == "/api/self-repair/healing/start": self._json({"ok": True, "healing": self.repair.start_healing(payload)}) return if path == "/api/self-repair/runtime": self._json({"ok": True, "runtime": self.repair.set_runtime(payload)}) return if path == "/api/self-repair/runtime/reset-statistics": self._json({ "ok": True, "runtime": self.repair.reset_runtime_statistics(), }) return if path == "/api/self-repair/healing/run-all": self._json({"ok": True, "batch": self.repair.start_batch(payload)}) return if path == "/api/self-repair/healing/reset": self._json({"ok": True, **self.repair.reset_healing(payload)}) return if path == "/api/self-repair/hardware/start": self._json({"ok": True, "session": self.repair.start_hardware()}) return if path == "/api/self-repair/hardware/stop": self._json({"ok": True, "session": self.repair.stop_hardware()}) return if path == "/api/self-repair/hardware/self-test": self._json( {"ok": True, "self_test": self.repair.run_hardware_self_test()} ) return if path == "/api/self-repair/emergency-stop": self._json({"ok": True, "request": self.repair.emergency_stop()}) return if path == "/api/self-repair/snapshot": self._json({"ok": True, "path": str(self.repair.save_snapshot())}) return if path == "/api/self-repair/firmware/build-flash": self._json( { "ok": True, "firmware": self.repair.build_and_flash_firmware(payload), } ) return if path == "/api/self-repair/open-log": opened = self.repair.open_log_dir() self._json({"ok": opened}) return self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND) except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc: self._json( {"ok": False, "error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST, ) except Exception as exc: # noqa: BLE001 - keep self-repair API responsive self.log_error("self-repair request failed: %s", exc) self._json( { "ok": False, "error": "internal_error", "message": "自修复服务处理失败,请查看服务端日志。", }, HTTPStatus.INTERNAL_SERVER_ERROR, ) def _require_self_repair_admin(self, parsed_url: ParseResult | None = None) -> bool: role = self.headers.get("X-Cognitive-Agent-Role", "").strip().lower() if not role and parsed_url is not None: role = str(parse_qs(parsed_url.query).get("role", [""])[0]).lower() if hmac.compare_digest(role, "admin"): return True self._json( {"error": "admin_required", "message": "自修复仅对 admin 开放。"}, HTTPStatus.FORBIDDEN, ) return False def _require_calibration_admin(self, parsed_url: ParseResult | None = None) -> bool: role = self.headers.get("X-Cognitive-Agent-Role", "").strip().lower() if not role and parsed_url is not None: role = str(parse_qs(parsed_url.query).get("role", [""])[0]).lower() if hmac.compare_digest(role, "admin"): return True self._json( { "error": "admin_required", "message": "自标定仅对 admin 开放。", }, HTTPStatus.FORBIDDEN, ) return False def _serve_calibration_events(self) -> None: self.send_response(HTTPStatus.OK) self.send_header("Content-Type", "text/event-stream; charset=utf-8") self.send_header("Cache-Control", "no-cache") self.send_header("Connection", "keep-alive") self.send_header("X-Accel-Buffering", "no") self._send_security_headers() self.end_headers() version = -1 try: while True: state = self.calibration.wait_for_version(version, 12.0) version = state["version"] data = json.dumps(state, ensure_ascii=False, separators=(",", ":")) self.wfile.write(f"data: {data}\n\n".encode("utf-8")) self.wfile.flush() except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError): pass def _serve_calibration_video(self) -> None: self._json( { "error": "legacy_video_transport_removed", "message": "ZED 画面已改为浏览器直连 WebRTC/WHEP。", }, HTTPStatus.GONE, ) def _handle_extension_gateway(self, action: str, payload: dict[str, object]) -> None: try: result = self.extension.execute_gateway(action, dict(payload)) if action != "info": self.vehicle_display_sync.update_extension( action={"type": action, **dict(payload)}, result=result, ) self._json(result) except GatewayError as exc: self._json( {"success": False, "error": "gscu_unavailable", "message": str(exc)}, HTTPStatus.SERVICE_UNAVAILABLE, ) def _extension_health(self) -> dict[str, object]: return { **self.extension.health(), "nfc": self.nfc.snapshot(), "pet_cards": self.pets.snapshot(), "extension_toy": self.extension_toys.snapshot(), "extension_control": self.extension_control.snapshot(), } def _handle_hardware_access_post(self, path: str) -> None: local_path = path.startswith("/api/local/") if local_path and not self._require_local_decision_point(): return try: payload = self._read_json_body(max_bytes=4096) client_id = str(payload.get("client_id", "")) if path == "/api/hardware/access/request": status = self.service.hardware_access.request_access( client_id, str(payload.get("label", "")), self._client_key(), ) self._json( status, HTTPStatus.OK if status["state"] == "approved" else HTTPStatus.ACCEPTED, ) return if path == "/api/hardware/access/release": self._json(self.service.hardware_access.release(client_id)) return if path == "/api/local/hardware-access/approve": self._json(self.service.hardware_access.approve(client_id)) return if path == "/api/local/hardware-access/deny": self._json(self.service.hardware_access.deny(client_id)) return if path == "/api/local/hardware-access/revoke": self._json( self.service.hardware_access.revoke(client_id or None) ) return except (ValueError, json.JSONDecodeError) as exc: self._json( {"error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST, ) def _read_json_body(self, max_bytes: int = 32768) -> dict[str, object]: content_length = int(self.headers.get("Content-Length", "0")) if content_length <= 0 or content_length > max_bytes: raise ValueError(f"request body must be between 1 and {max_bytes} bytes") payload = json.loads(self.rfile.read(content_length).decode("utf-8")) if not isinstance(payload, dict): raise ValueError("request body must be a JSON object") return payload def _require_json_content_type(self) -> bool: if is_json_content_type(self.headers.get("Content-Type", "")): return True self._json( { "error": "unsupported_media_type", "message": "请求 Content-Type 必须为 application/json。", }, HTTPStatus.UNSUPPORTED_MEDIA_TYPE, ) return False def _require_same_origin(self) -> bool: """Reject browser cross-origin mutations while retaining local CLI calls.""" origin = self.headers.get("Origin", "").strip() if not origin: return True scheme = "https" if self._is_https() else "http" if origin_matches_host(origin, self.headers.get("Host", ""), scheme): return True self._json( { "error": "cross_origin_request", "message": "拒绝跨来源的状态变更请求。", }, HTTPStatus.FORBIDDEN, ) return False def _handle_speech_transcription(self) -> None: content_type = self.headers.get("Content-Type", "").split(";", 1)[0].strip().lower() if content_type not in {"audio/wav", "audio/wave", "audio/x-wav"}: self._json( { "error": "unsupported_media_type", "message": "语音听写接口仅接受 16 位 PCM WAV 音频。", }, HTTPStatus.UNSUPPORTED_MEDIA_TYPE, ) return try: content_length = int(self.headers.get("Content-Length", "0")) if content_length <= 0 or content_length > 2 * 1024 * 1024: raise SpeechRecognitionError( "invalid_audio_size", "录音数据为空或超过大小限制。", status=413, ) transcription_arguments: dict[str, object] = { "utterance_id": self.headers.get("X-Speech-Utterance-Id") } requested_mode = ( self.headers.get("X-Speech-Input-Mode", "").strip().lower() ) legacy_wake_required = ( self.headers.get("X-Speech-Wake-Word-Required", "").strip() == "1" ) input_mode = requested_mode or ( "wake" if legacy_wake_required else "manual" ) if input_mode not in {"manual", "wake"}: raise SpeechRecognitionError( "invalid_speech_input_mode", "语音输入模式仅支持 manual 或 wake。", status=400, ) if requested_mode == "manual" and legacy_wake_required: raise SpeechRecognitionError( "speech_input_mode_conflict", "手动语音模式不能同时要求唤醒词。", status=400, ) transcription_arguments["require_wake_word"] = input_mode == "wake" if bool(getattr(self.speech, "supports_input_modes", False)): transcription_arguments["input_mode"] = input_mode result = self.speech.transcribe_wav( self.rfile.read(content_length), **transcription_arguments, ) result = {**result, "input_mode": input_mode} raw_text = str(result.get("raw_text") or "").strip() peripheral_routing = self._speech_peripheral_routing(raw_text) if peripheral_routing.get("routing_eligible"): result = { **result, "routing_text": raw_text, "peripheral_routing": peripheral_routing, } self._json(result) except SpeechRecognitionError as exc: self._json( {"error": exc.code, "message": str(exc)}, HTTPStatus(exc.status), ) except (TypeError, ValueError): self._json( {"error": "invalid_audio", "message": "录音请求格式无效。"}, HTTPStatus.BAD_REQUEST, ) def _handle_speech_preview(self) -> None: """Return semantic-free partial text for an in-progress microphone session.""" content_type = self.headers.get("Content-Type", "").split(";", 1)[0].strip().lower() if content_type not in {"audio/wav", "audio/wave", "audio/x-wav"}: self._json( { "error": "unsupported_media_type", "message": "实时语音接口仅接受 16 位 PCM WAV 音频。", }, HTTPStatus.UNSUPPORTED_MEDIA_TYPE, ) return slot_acquired = False content_length = 0 request_body_consumed = False try: content_length = int(self.headers.get("Content-Length", "0")) if content_length <= 0 or content_length > 2 * 1024 * 1024: raise SpeechRecognitionError( "invalid_audio_size", "实时录音数据为空或超过大小限制。", status=413, ) sequence = int(self.headers.get("X-Speech-Preview-Sequence", "-1")) if sequence < 0 or sequence > 100_000: raise SpeechRecognitionError( "invalid_preview_sequence", "实时转写序号无效。", status=400, ) if not self.speech_preview_http_slots.acquire(blocking=False): raise SpeechRecognitionError( "preview_overloaded", "实时语音请求过多,请稍后重试。", status=429, retry_after_seconds=1, ) slot_acquired = True preview_arguments: dict[str, object] = { "utterance_id": self.headers.get("X-Speech-Utterance-Id"), "sequence": sequence, } input_mode = ( self.headers.get("X-Speech-Input-Mode", "wake").strip().lower() or "wake" ) if input_mode not in {"manual", "wake"}: raise SpeechRecognitionError( "invalid_speech_input_mode", "语音输入模式仅支持 manual 或 wake。", status=400, ) supports_input_modes = bool( getattr(self.speech, "supports_input_modes", False) ) if supports_input_modes: preview_arguments["input_mode"] = input_mode content = self.rfile.read(content_length) request_body_consumed = True result = self.speech.transcribe_preview_wav( content, **preview_arguments, ) if input_mode == "manual" and not supports_input_modes: raw_text = str(result.get("raw_text") or result.get("text") or "") wake_word = dict(result.get("wake_word") or {}) wake_word.update({"required": False, "remainder": raw_text}) result = { **result, "raw_text": raw_text, "text": raw_text, "live_text": raw_text, "wake_word": wake_word, } result = {**result, "input_mode": input_mode} self._json(result) except SpeechRecognitionError as exc: error_payload: dict[str, object] = { "error": exc.code, "message": str(exc), } error_headers: dict[str, str] = {} if exc.retry_after_seconds is not None: error_payload["retry_after_seconds"] = exc.retry_after_seconds error_headers["Retry-After"] = str(exc.retry_after_seconds) self._json( error_payload, HTTPStatus(exc.status), error_headers, ) if not request_body_consumed: self._drain_rejected_request_body(content_length) except (TypeError, ValueError): self._json( {"error": "invalid_preview", "message": "实时语音请求格式无效。"}, HTTPStatus.BAD_REQUEST, ) if not request_body_consumed: self._drain_rejected_request_body(content_length) finally: if slot_acquired: self.speech_preview_http_slots.release() def _drain_rejected_request_body( self, content_length: int, *, max_bytes: int = 2 * 1024 * 1024, ) -> None: """Gracefully finish an early-rejected POST without processing its payload. Closing a Windows socket while its peer's request body is still unread can replace an already-written HTTP error with TCP RST. Flush the rejection first, then briefly discard a valid-sized body so clients reliably receive the 4xx response. Callers choose a strict size ceiling, and authorization or concurrency gates still run before any payload is read or decoded. """ if content_length <= 0 or content_length > max_bytes: return self.close_connection = True try: self.wfile.flush() except OSError: return previous_timeout = self.connection.gettimeout() try: self.connection.settimeout(0.25) remaining = content_length while remaining > 0: chunk = self.rfile.read(min(remaining, 64 * 1024)) if not chunk: break remaining -= len(chunk) except (OSError, ValueError): pass finally: try: self.connection.settimeout(previous_timeout) except OSError: pass def _authorize(self, path: str) -> bool: if not self.auth.config.enabled or path in PUBLIC_AUTH_PATHS: return True if self._session_username() is not None: return True if path.startswith("/api/"): self._json( { "error": "authentication_required", "message": "请登录后访问。", }, HTTPStatus.UNAUTHORIZED, ) else: requested = path if path.startswith("/") else "/" self._redirect(f"/login?next={quote(requested, safe='/')}") return False def _login(self) -> None: if not self.auth.config.enabled: self._json({"enabled": False, "authenticated": True}) return client_key = self._client_key() retry_after = self.auth.login_failures.retry_after(client_key) if retry_after: self._json( { "error": "login_rate_limited", "message": "登录尝试过多,请稍后再试。", }, HTTPStatus.TOO_MANY_REQUESTS, {"Retry-After": str(retry_after)}, ) return try: content_length = int(self.headers.get("Content-Length", "0")) if content_length <= 0 or content_length > 4096: raise ValueError("invalid login body") payload = json.loads(self.rfile.read(content_length).decode("utf-8")) if not isinstance(payload, dict): raise ValueError("invalid login body") username = str(payload.get("username", ""))[:128] password = str(payload.get("password", ""))[:1024] except (ValueError, json.JSONDecodeError): self._json( {"error": "invalid_request", "message": "登录请求格式无效。"}, HTTPStatus.BAD_REQUEST, ) return password_valid = verify_password(password, self.auth.config.password_hash) username_valid = hmac.compare_digest( username.encode("utf-8"), self.auth.config.username.encode("utf-8"), ) if not (username_valid and password_valid): self.auth.login_failures.record(client_key) self._json( {"error": "invalid_credentials", "message": "用户名或密码错误。"}, HTTPStatus.UNAUTHORIZED, ) return self.auth.login_failures.clear(client_key) token = self.auth.sessions.issue(self.auth.config.username) attributes = [ f"{self.auth.config.cookie_name}={token}", "Path=/", "HttpOnly", "SameSite=Strict", f"Max-Age={self.auth.config.session_ttl_seconds}", ] if self._is_https(): attributes.append("Secure") self._json( { "authenticated": True, "username": self.auth.config.username, }, headers={"Set-Cookie": "; ".join(attributes)}, ) def _logout(self) -> None: attributes = [ f"{self.auth.config.cookie_name}=", "Path=/", "HttpOnly", "SameSite=Strict", "Max-Age=0", ] if self._is_https(): attributes.append("Secure") self._json( {"authenticated": False}, headers={"Set-Cookie": "; ".join(attributes)}, ) def _session_username(self) -> str | None: if not self.auth.config.enabled: return None cookie_header = self.headers.get("Cookie", "") if not cookie_header or len(cookie_header) > 4096: return None try: cookies = SimpleCookie() cookies.load(cookie_header) morsel = cookies.get(self.auth.config.cookie_name) return self.auth.sessions.verify(morsel.value if morsel else "") except (CookieError, ValueError): return None def _client_key(self) -> str: address = self.client_address[0] direct_address = ipaddress.ip_address(address) if self.auth.config.trust_proxy_headers and direct_address.is_loopback: forwarded = ( self.headers.get("CF-Connecting-IP") or self.headers.get("X-Forwarded-For", "").split(",", 1)[0] ).strip() try: address = str(ipaddress.ip_address(forwarded)) except ValueError: pass return address def _is_unproxied_loopback_request(self) -> bool: """Distinguish the local decision console from traffic via the tunnel.""" try: direct_address = ipaddress.ip_address(self.client_address[0]) except ValueError: return False proxy_headers = ( "Forwarded", "X-Forwarded-For", "X-Real-IP", "CF-Connecting-IP", ) return direct_address.is_loopback and not any( self.headers.get(name, "").strip() for name in proxy_headers ) def _hardware_access_bypass_allowed(self) -> bool: """Allow implicit hardware access only for authenticated local sessions.""" return ( self.auth.config.enabled and self._is_unproxied_loopback_request() and self._session_username() is not None ) def _passive_state_access_bypass_allowed( self, control_transport: str, ) -> bool: """Keep cached MQTT telemetry readable without widening control access. The wireless state endpoints only consume the process-local snapshot populated by the persistent MQTT subscription; they never publish a frame or reserve the local CAN bench. Mutating control handlers must continue to call ``_hardware_access_bypass_allowed`` directly. """ return ( control_transport == WIRELESS_MQTT_TRANSPORT or self._hardware_access_bypass_allowed() ) def _require_local_decision_point(self) -> bool: if self._is_unproxied_loopback_request(): return True self._json( { "error": "local_access_required", "message": "真实台架审批与停止权限仅在台架本机开放。", }, HTTPStatus.FORBIDDEN, ) return False def _is_https(self) -> bool: if not self.auth.config.trust_proxy_headers: return False if self.headers.get("X-Forwarded-Proto", "").lower() == "https": return True return '"scheme":"https"' in self.headers.get("CF-Visitor", "").replace(" ", "") def _serve_request(self, request_id: str) -> None: if not REQUEST_ID_PATTERN.fullmatch(request_id): self._json({"error": "invalid_request_id"}, HTTPStatus.BAD_REQUEST) return result_path = self.service.artifact_root / request_id / "result.json" if not result_path.is_file(): self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND) return self._json(json.loads(result_path.read_text(encoding="utf-8"))) def _serve_static(self, path: str, query: str = "") -> None: relative_path = "index.html" if path in {"", "/"} else path.lstrip("/") versioned = bool(parse_qs(query).get("v")) self._serve_static_file(relative_path, versioned=versioned) def _serve_static_file(self, relative_path: str, *, versioned: bool = False) -> None: candidate = (STATIC_ROOT / relative_path).resolve() try: candidate.relative_to(STATIC_ROOT.resolve()) except ValueError: self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND) return if not candidate.is_file() and ( Path(relative_path).suffix or relative_path.startswith(("assets/", "vendor/", "icons/")) ): self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND) return if not candidate.is_file(): candidate = STATIC_ROOT / "index.html" content_type, _ = mimetypes.guess_type(candidate.name) content = candidate.read_bytes() self.send_response(HTTPStatus.OK) resolved_content_type = content_type or "application/octet-stream" if resolved_content_type.startswith("text/") or resolved_content_type in { "application/javascript", "application/json", "application/manifest+json", "image/svg+xml", }: resolved_content_type = f"{resolved_content_type}; charset=utf-8" self.send_header( "Content-Type", resolved_content_type, ) self.send_header("Content-Length", str(len(content))) cache_control = "no-cache" if relative_path.startswith(("vendor/", "icons/")) or ( relative_path.startswith("assets/") and versioned ): cache_control = "public, max-age=31536000, immutable" elif relative_path.startswith("assets/"): cache_control = "no-cache" elif candidate.name == "service-worker.js": cache_control = "no-store" elif candidate.name in {"index.html", "login.html"}: cache_control = "no-cache" self.send_header("Cache-Control", cache_control) if candidate.name == "service-worker.js": self.send_header("Service-Worker-Allowed", "/") self._send_security_headers() self.end_headers() self.wfile.write(content) def _serve_gesture_model(self) -> None: path = self.gesture.model_path if not path.is_file(): self._json( {"error": "gesture_model_unavailable", "message": "手势模型文件不可用。"}, HTTPStatus.NOT_FOUND, ) return content = path.read_bytes() self.send_response(HTTPStatus.OK) self.send_header("Content-Type", "application/octet-stream") self.send_header("Content-Length", str(len(content))) self.send_header("Cache-Control", "private, max-age=3600") self._send_security_headers() self.end_headers() self.wfile.write(content) def _json( self, payload: object, status: HTTPStatus = HTTPStatus.OK, headers: dict[str, str] | None = None, ) -> None: content = json.dumps(payload, ensure_ascii=False).encode("utf-8") self.send_response(status) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", str(len(content))) self.send_header("Cache-Control", "no-store") for name, value in (headers or {}).items(): self.send_header(name, value) self._send_security_headers() self.end_headers() self.wfile.write(content) def _redirect(self, location: str) -> None: self.send_response(HTTPStatus.SEE_OTHER) self.send_header("Location", location) self.send_header("Cache-Control", "no-store") self._send_security_headers() self.end_headers() def _send_security_headers(self) -> None: self.send_header("X-Content-Type-Options", "nosniff") self.send_header("X-Frame-Options", "DENY") self.send_header("Referrer-Policy", "no-referrer") self.send_header( "Permissions-Policy", "camera=(self), microphone=(self), geolocation=(), payment=(), usb=()", ) self.send_header( "Content-Security-Policy", "default-src 'self'; " "script-src 'self' 'wasm-unsafe-eval'; " "style-src 'self'; " "img-src 'self' data: blob:; " "media-src 'self' blob:; " "frame-src 'self' http://*:3001; " "connect-src 'self' blob: http://172.20.10.6:8889; " "object-src 'none'; base-uri 'none'; frame-ancestors 'none'", ) if self._is_https(): self.send_header( "Strict-Transport-Security", "max-age=31536000; includeSubDomains", ) class N7WebHttpServer(ThreadingHTTPServer): """Prevent multiple Windows processes from silently sharing one port.""" daemon_threads = True allow_reuse_address = os.name != "nt" def server_bind(self) -> None: if os.name == "nt" and hasattr(socket, "SO_EXCLUSIVEADDRUSE"): self.socket.setsockopt( socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1, ) super().server_bind() def main() -> None: parser = argparse.ArgumentParser(description="Run the cabin orchestration demo") parser.add_argument("--host", default=os.getenv("HOST", "127.0.0.1")) parser.add_argument("--port", type=int, default=int(os.getenv("PORT", "8000"))) parser.add_argument( "--scene-catalog", type=Path, default=Path( os.getenv("N7_SCENE_CATALOG_PATH", "").strip() or DEFAULT_MODE_SCENE_CATALOG_PATH ), help="JSON path synchronized from preset and user-created mode cards", ) parser.add_argument( "--scene-server-id", default=os.getenv("N7_SCENE_SERVER_ID", "n7-cognitive-agent"), help="server_id written to the N7 scene catalog", ) args = parser.parse_args() scene_catalog_status = SeatDemoHandler.service.configure_mode_scene_catalog( args.scene_catalog, server_id=args.scene_server_id, ) server = N7WebHttpServer((args.host, args.port), SeatDemoHandler) SeatDemoHandler.speech.start() SeatDemoHandler.assistant.start( f"http://127.0.0.1:{server.server_port}/api/internal/codex/vehicle-control" ) SeatDemoHandler.service.start() SeatDemoHandler.extension.start() SeatDemoHandler.extension_control.start() SeatDemoHandler.nfc.start() SeatDemoHandler.pets.start() SeatDemoHandler.extension_toys.start() SeatDemoHandler.calibration.start() SeatDemoHandler.repair.start() if args.host in {"0.0.0.0", "::"}: lan_address = _lan_ipv4_address() print(f"Cabin orchestration demo (local): http://127.0.0.1:{args.port}", flush=True) if lan_address: print( f"Cabin orchestration demo (LAN): http://{lan_address}:{args.port}", flush=True, ) print( "LAN mode: devices on reachable networks can use the GPT-backed API.", flush=True, ) else: print(f"Cabin orchestration demo: http://{args.host}:{args.port}", flush=True) if scene_catalog_status["state"] == "ready": print( "N7 mode scene catalog: " f"{scene_catalog_status['path']} " f"({scene_catalog_status['scene_count']} scenes)", flush=True, ) else: print( "N7 mode scene catalog synchronization unavailable: " f"{scene_catalog_status.get('last_error') or 'unknown error'}", file=sys.stderr, flush=True, ) print( "N7 new-mode MQTT sync: " f"{MQTT_MODE_SCENE_TOPIC} " f"({MODE_SCENE_MQTT_BURST_COUNT} frames within " f"{round(MODE_SCENE_MQTT_BURST_WINDOW_S * 1_000)} ms)", flush=True, ) print( "N7 extension capability MQTT: " f"{MQTT_EXTENSION_TOY_TOPIC} " f"({EXTENSION_TOY_BURST_COUNT} frames within " f"{round(EXTENSION_TOY_BURST_WINDOW_S * 1_000)} ms per NFC detection)", flush=True, ) print( "N7 extension control MQTT: " f"subscribe {MQTT_EXTENSION_CONTROL_TOPIC} -> local infrared gateway", flush=True, ) hardware_execution_ready = any( ( SeatDemoHandler.service.hardware_settings.configured, SeatDemoHandler.service.vehicle_can_settings.configured, SeatDemoHandler.service.vehicle_mqtt_settings.configured, ) ) print( "Execution modes: dry_run + simulated_hardware " f"({'enabled' if SeatDemoHandler.service.simulated_hardware_enabled else 'locked'}) " "+ hardware " f"({'configured' if hardware_execution_ready else 'locked'})", flush=True, ) try: server.serve_forever() except KeyboardInterrupt: pass finally: server.server_close() SeatDemoHandler.speech.close() SeatDemoHandler.assistant.close() SeatDemoHandler.service.set_extension_control_handler(None) SeatDemoHandler.service.set_extension_control_rejection_handler(None) SeatDemoHandler.extension_control.close() SeatDemoHandler.extension_toys.close() SeatDemoHandler.pets.close() SeatDemoHandler.nfc.close() SeatDemoHandler.service.close() SeatDemoHandler.extension.close() SeatDemoHandler.calibration.close() SeatDemoHandler.repair.close() if __name__ == "__main__": main()