feat(n7web): sync N7 scene cards and optimize runtime

This commit is contained in:
Miayui
2026-09-17 11:04:05 +08:00
parent 75ce391312
commit 7d196db02b
31 changed files with 2924 additions and 126 deletions
+454 -9
View File
@@ -2,14 +2,16 @@
from __future__ import annotations
import hashlib
import json
import os
import re
import sys
import threading
import time
import uuid
from collections import Counter
from collections.abc import Callable
from collections.abc import Callable, Mapping
from concurrent.futures import ThreadPoolExecutor
from copy import deepcopy
from datetime import UTC, datetime
@@ -50,6 +52,10 @@ from cognitive_agent.application.domain_runtime import ConfiguredDomainRuntime
from cognitive_agent.application.mode_profile import ( # noqa: E402
ModeProfile,
ModeProfileRegistry,
ModeTimeline,
)
from cognitive_agent.application.mode_scene_ingress import ( # noqa: E402
ModeSceneIngressConsumer,
)
from cognitive_agent.application.mode_scene_catalog import ( # noqa: E402
DEFAULT_N7_SCENE_SERVER_ID,
@@ -130,7 +136,9 @@ from cognitive_agent.infrastructure.can.hardware import ( # noqa: E402
VehicleMqttSettings,
)
from cognitive_agent.infrastructure.can.mqtt_transport import ( # noqa: E402
MQTT_MODE_SCENE_INGRESS_TOPIC,
MQTT_MODE_SCENE_TOPIC,
MQTT_N7_CONTROL_TRANSFER_TOPIC,
MqttCanTransport,
)
from cognitive_agent.infrastructure.llm.openai_responses import ( # noqa: E402
@@ -446,6 +454,14 @@ class SeatDemoService:
lambda scene: self._vehicle_mqtt_transport.publish_mode_scene(scene),
topic=MQTT_MODE_SCENE_TOPIC,
)
self._mode_scene_ingress = ModeSceneIngressConsumer(
self.import_n7_mode_scene,
)
self._vehicle_mqtt_transport.set_mode_scene_ingress_handler(
self._mode_scene_ingress.submit
)
self._n7_transfer_lock = threading.RLock()
self._n7_transfer_publications: dict[str, float] = {}
self.scene_templates = SceneTemplateRegistry(
repository_root / "config" / "scene_templates"
)
@@ -546,6 +562,18 @@ class SeatDemoService:
def mode_scene_catalog_status(self) -> dict[str, Any]:
return deepcopy(self._mode_scene_catalog_status)
def mode_profile_sync_status(self) -> dict[str, Any]:
"""Return the lightweight state used by the browser's scene watcher."""
catalog = self.mode_scene_catalog_status()
return {
"catalog_revision": catalog.get("catalog_revision"),
"scene_count": int(catalog.get("scene_count") or 0),
"catalog_state": str(catalog.get("state") or "unknown"),
"catalog_error": catalog.get("last_error"),
"mode_scene_ingress": self._mode_scene_ingress.snapshot(),
}
def publish_saved_mode_scene(
self,
mode_id: str,
@@ -591,6 +619,142 @@ class SeatDemoService:
return self.publish_saved_mode_scene(mode_id, vehicle_id)
@staticmethod
def _imported_scene_function_configs(
scene: dict[str, Any],
) -> dict[str, dict[str, Any]]:
"""Retain only local peripheral metadata needed for later authorization."""
extension_operations: list[dict[str, Any]] = []
for index, action in enumerate(scene.get("actions", [])):
if (
not isinstance(action, dict)
or action.get("domain") != "self_extension"
or action.get("capability") != "infrared.emit"
):
continue
target = action.get("target")
target_document = (
deepcopy(target)
if isinstance(target, dict)
else {"device": str(action.get("target_scope") or "")}
)
extension_operations.append(
{
"id": str(action.get("operation_ref") or f"n7-ir-{index + 1}"),
"domain": "self_extension",
"capability": "infrared.emit",
"target_scope": str(action.get("target_scope") or ""),
"target": target_document,
"operation": str(action.get("operation") or "emit"),
"parameters": deepcopy(dict(action.get("parameters") or {})),
"parse_source": "from_n7_scene",
}
)
if not extension_operations:
return {}
return {
"self_extension": {
"apiVersion": "cognitive-agent.io/v1alpha1",
"kind": "FunctionConfig",
"metadata": {
"id": f"from-n7-{scene['id']}-self-extension",
"name": f"{scene['name']} imported infrared actions",
"labels": {"source_topic": MQTT_MODE_SCENE_INGRESS_TOPIC},
},
"spec": {
"domain": "self_extension",
"operations": extension_operations,
},
}
}
def import_n7_mode_scene(
self,
scene: Mapping[str, Any],
) -> dict[str, Any]:
"""Upsert one validated N7 card without publishing it back to the broker."""
document = deepcopy(dict(scene))
actions = document.get("actions")
if not isinstance(actions, list) or not actions:
raise ValueError("from_n7/scenes card must contain executable actions")
operations = [
{
"domain": str(action["domain"]),
"capability": str(action["capability"]),
"target_scope": str(action["target_scope"]),
"operation": str(action["operation"]),
"parameters": deepcopy(dict(action["parameters"])),
"reason": (
f"由 {MQTT_MODE_SCENE_INGRESS_TOPIC} 导入的模式动作。"
),
}
for action in actions
]
timeline = None
raw_timeline = document.get("timeline")
if raw_timeline is not None:
if not isinstance(raw_timeline, dict):
raise ValueError("from_n7/scenes timeline must be an object")
timeline = ModeTimeline.from_dict(raw_timeline)
command_flow = {
"schema": "cognitive-agent-mode-command-flow/v1",
"source_topic": MQTT_MODE_SCENE_INGRESS_TOPIC,
"imported_at": _now(),
"source_scene": document,
"function_configs": self._imported_scene_function_configs(document),
# The execution path recompiles the canonical ModeOperation list
# against this PC's capability registry. This marker makes the
# card directly executable while avoiding trust in foreign CAN bytes.
"plans": {
"from_n7_scene": {
"spec": {
"commands": [
{
"operation_ref": str(
action.get("operation_ref") or f"action-{index + 1}"
),
"at_ms": int(action.get("at_ms", 0)),
}
for index, action in enumerate(actions)
]
}
}
},
"execution_steps": [],
}
scope = document.get("scope") or {}
vehicle_ids = scope.get("vehicle_ids", []) if isinstance(scope, dict) else []
common = bool(scope.get("common", not vehicle_ids)) if isinstance(scope, dict) else False
resolved_vehicle_id = (
None
if common and not vehicle_ids
else self.vehicle_profiles.default().id
)
saved = self.mode_profiles.save_mode(
str(document["name"]),
operations,
mode_id=str(document["id"]),
aliases=[str(alias) for alias in document.get("aliases", [])],
vehicle_id=resolved_vehicle_id,
description=str(document.get("description") or "由 N7 同步的模式卡片。"),
icon=str(document.get("icon") or "radio-tower"),
instruction=f"进入{document['name']}",
command_flow=command_flow,
timeline=timeline,
sync_origin=MQTT_MODE_SCENE_INGRESS_TOPIC,
upstream_origin=str(document.get("origin") or ""),
)
return {
"status": "imported",
"scene_id": saved.id,
"scene_name": saved.name,
"source_topic": MQTT_MODE_SCENE_INGRESS_TOPIC,
"profile_revision": self.mode_profiles.profile_revision(saved),
"storage": "persistent_yaml",
}
@property
def driver_only_ch1_execution_active(self) -> bool:
"""Return whether deployment policy authorizes only the CH1 vehicle path."""
@@ -646,6 +810,7 @@ class SeatDemoService:
"last_error": None,
}
vehicle_mqtt_info = self._vehicle_mqtt_transport.public_info()
vehicle_mqtt_info["mode_scene_import"] = self._mode_scene_ingress.snapshot()
return {
"service": "cabin-cognitive-orchestrator",
"domain": "cabin",
@@ -807,6 +972,207 @@ class SeatDemoService:
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
return self.mode_profiles.get_profile(mode_id, resolved_vehicle_id)
@staticmethod
def _fallback_scene_action(
operation: Any,
*,
index: int,
) -> dict[str, Any]:
return {
"type": "control",
"at_ms": 0,
"domain": operation.domain,
"capability": operation.capability,
"target_scope": operation.target_scope,
"operation": operation.operation,
"parameters": deepcopy(operation.parameters),
"operation_ref": f"action-{index + 1}",
}
def _mode_requests_and_n7_transfers(
self,
profile: ModeProfile,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
"""Separate locally understood operations from N7-only capabilities."""
requests = self.mode_profiles.requests_for_profiles([profile])
if profile.sync_origin != MQTT_MODE_SCENE_INGRESS_TOPIC:
return requests, []
source_scene = (
profile.command_flow.get("source_scene")
if isinstance(profile.command_flow, dict)
else None
)
source_actions = (
source_scene.get("actions")
if isinstance(source_scene, dict)
and isinstance(source_scene.get("actions"), list)
else []
)
local_requests: list[dict[str, Any]] = []
transfer_actions: list[dict[str, Any]] = []
for index, (operation, request) in enumerate(
zip(profile.operations, requests, strict=True)
):
if (
operation.domain == "self_extension"
and operation.capability == "infrared.emit"
):
# The server's durable NFC allowlist owns this local TCP path.
# Authorization failures must not be converted into N7 retries.
continue
registered = self.model_plan_adapter.capabilities.get(
operation.capability
)
native_candidate = {
"domain": operation.domain,
"capability": operation.capability,
"target": operation.target_scope,
"parameters": deepcopy(operation.parameters),
"operation": operation.operation,
}
if registered is not None or self.native_can_runtime.supports(
native_candidate
):
local_requests.append(request)
continue
raw_action = (
source_actions[index]
if index < len(source_actions)
and isinstance(source_actions[index], dict)
else None
)
action = (
deepcopy(raw_action)
if raw_action is not None
else self._fallback_scene_action(operation, index=index)
)
transfer_actions.append(action)
return local_requests, transfer_actions
@staticmethod
def _has_local_mode_peripheral(profile: ModeProfile) -> bool:
return any(
operation.domain == "self_extension"
and operation.capability == "infrared.emit"
for operation in profile.operations
)
@staticmethod
def _n7_transfer_deferred_requests(
actions: list[dict[str, Any]],
) -> list[dict[str, Any]]:
return [
{
"domain": str(action.get("domain") or ""),
"capability": str(action.get("capability") or ""),
"target_scope": str(action.get("target_scope") or ""),
"desired_change": str(action.get("operation") or "set"),
"parameters": deepcopy(dict(action.get("parameters") or {})),
"reason": "当前 PC 未注册该能力,将由 N7 端继续处理。",
"execution_backend": "n7_vehicle_transfer",
"rejection_code": "PC_CAPABILITY_UNAVAILABLE",
"transport_submitted": False,
}
for action in actions
]
def _finalize_n7_control_transfer(
self,
result: dict[str, Any],
*,
execution_mode: str,
local_preview: bool,
) -> dict[str, Any]:
"""Forward only N7-originated actions never submitted by this PC."""
actions = result.get("n7_transfer_candidates")
scene = result.get("n7_source_scene")
if (
execution_mode != "hardware"
or local_preview
or not isinstance(actions, list)
or not actions
or not isinstance(scene, dict)
):
return result
if str(result.get("status") or "") not in {
"succeeded",
"partially_succeeded",
"capability_unavailable",
}:
return result
if result.get("execution_outcome") == "failed_or_blocked":
return result
request_id = str(result.get("request_id") or "").strip()
scene_id = str(scene.get("id") or "").strip()
scene_name = str(scene.get("name") or "").strip()
if not request_id or not scene_id or not scene_name:
return result
transfer_actions = [deepcopy(action) for action in actions if isinstance(action, dict)]
if not transfer_actions:
return result
payload = {
"scene_id": scene_id,
"scene_name": scene_name,
"request_id": request_id,
"operation_count": len(transfer_actions),
"actions": transfer_actions,
}
digest = json.dumps(
payload,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
publication_key = f"{request_id}:{hashlib.sha256(digest.encode('utf-8')).hexdigest()}"
now = time.monotonic()
with self._n7_transfer_lock:
cutoff = now - 300.0
self._n7_transfer_publications = {
key: published_at
for key, published_at in self._n7_transfer_publications.items()
if published_at >= cutoff
}
if publication_key in self._n7_transfer_publications:
result["n7_control_transfer"] = {
"state": "duplicate_ignored",
"topic": MQTT_N7_CONTROL_TRANSFER_TOPIC,
"scene_id": scene_id,
"operation_count": len(transfer_actions),
}
return result
try:
publication = self._vehicle_mqtt_transport.publish_n7_control_transfer(
payload
)
except Exception as exc: # noqa: BLE001 - local execution result remains valid
result["n7_control_transfer"] = {
"state": "publish_failed",
"topic": MQTT_N7_CONTROL_TRANSFER_TOPIC,
"scene_id": scene_id,
"operation_count": len(transfer_actions),
"last_error": str(exc)[:240],
}
return result
with self._n7_transfer_lock:
self._n7_transfer_publications[publication_key] = now
result["n7_control_transfer"] = {
"state": "published",
"scene_id": scene_id,
"operation_count": len(transfer_actions),
"payload": payload,
**dict(publication),
}
result["decision_summary"] = [
*list(result.get("decision_summary") or []),
(
f"当前 PC 未注册的 {len(transfer_actions)} 条 N7 来源动作已发送至 "
f"{MQTT_N7_CONTROL_TRANSFER_TOPIC};PC 已提交或被安全门禁拦截的动作未转发。"
),
]
return result
def execute_mode_profile_card(
self,
mode_id: str,
@@ -1183,6 +1549,10 @@ class SeatDemoService:
instruction=clean_instruction,
command_flow=command_flow,
timeline=profile.timeline if profile is not None else None,
sync_origin=profile.sync_origin if profile is not None else "",
upstream_origin=(
profile.upstream_origin if profile is not None else ""
),
expected_revision=draft_existing_mode_revision or None,
create_only=not bool(clean_mode_id),
)
@@ -1379,6 +1749,7 @@ class SeatDemoService:
if self._vehicle_state_monitor is not None:
self._vehicle_state_monitor.start()
self._mode_scene_mqtt_publisher.start()
self._mode_scene_ingress.start()
self._vehicle_mqtt_transport.start()
def close(self) -> None:
@@ -1386,6 +1757,7 @@ class SeatDemoService:
self._vehicle_state_monitor.close()
self._mode_scene_mqtt_publisher.close()
self._vehicle_mqtt_transport.close()
self._mode_scene_ingress.close()
def publish_pet_message(self, payload: dict[str, str]) -> dict[str, Any]:
"""Publish one NFC-authorized pet card through the shared vehicle broker."""
@@ -2416,7 +2788,7 @@ class SeatDemoService:
# The head unit owns both physical CAN channels in wireless mode.
# Local T2G/ZLG hardware access, firmware profiles and the local
# physical-bench lease are unrelated to MQTT relay execution.
return self._process_once(
result = self._process_once(
utterance,
provider,
conversation_id,
@@ -2438,11 +2810,16 @@ class SeatDemoService:
mode_profile_draft_context=_mode_profile_draft_context,
cloud_instruction_context=_cloud_instruction_context,
)
return self._finalize_n7_control_transfer(
result,
execution_mode=execution_mode,
local_preview=_local_preview,
)
if hardware_access_bypass or self._driver_only_vehicle_execution_active(
control_transport
):
with self.hardware_execution.reserve():
return self._process_once(
result = self._process_once(
utterance,
provider,
conversation_id,
@@ -2464,10 +2841,15 @@ class SeatDemoService:
mode_profile_draft_context=_mode_profile_draft_context,
cloud_instruction_context=_cloud_instruction_context,
)
return self._finalize_n7_control_transfer(
result,
execution_mode=execution_mode,
local_preview=_local_preview,
)
authorized_client_id = self.hardware_access.ensure_authorized(hardware_client_id)
with self.hardware_execution.reserve():
with self.hardware_access.use(authorized_client_id):
return self._process_once(
result = self._process_once(
utterance,
provider,
conversation_id,
@@ -2489,6 +2871,11 @@ class SeatDemoService:
mode_profile_draft_context=_mode_profile_draft_context,
cloud_instruction_context=_cloud_instruction_context,
)
return self._finalize_n7_control_transfer(
result,
execution_mode=execution_mode,
local_preview=_local_preview,
)
def _process_once(
self,
@@ -2768,8 +3155,8 @@ class SeatDemoService:
traces,
)
if direct_mode_profile is not None:
mode_requests = self.mode_profiles.requests_for_profiles(
[direct_mode_profile]
mode_requests, n7_transfer_actions = (
self._mode_requests_and_n7_transfers(direct_mode_profile)
)
detected_domains = list(
dict.fromkeys(
@@ -2814,6 +3201,28 @@ class SeatDemoService:
*list(parsed.get("native_can_actions", [])),
*saved_native_actions,
]
if n7_transfer_actions:
parsed["deferred_requests"] = [
*list(parsed.get("deferred_requests", [])),
*self._n7_transfer_deferred_requests(
n7_transfer_actions
),
]
parsed["_n7_transfer_actions"] = n7_transfer_actions
parsed["_n7_scene_profile"] = {
"id": direct_mode_profile.id,
"name": direct_mode_profile.name,
}
parsed["detected_domains"] = self._merge_domains(
list(parsed.get("detected_domains", [])),
[
str(action.get("domain") or "")
for action in n7_transfer_actions
if str(action.get("domain") or "")
],
)
if self._has_local_mode_peripheral(direct_mode_profile):
parsed["_saved_mode_local_peripheral"] = True
provider_used = "saved_mode_command_flow"
provider_notice = None
elif direct_can_batch is not None:
@@ -2951,6 +3360,12 @@ class SeatDemoService:
)
),
)
n7_transfer_actions = parsed.get("_n7_transfer_actions", [])
n7_scene_profile = parsed.get("_n7_scene_profile")
if isinstance(n7_transfer_actions, list) and n7_transfer_actions:
result["n7_transfer_candidates"] = deepcopy(n7_transfer_actions)
if isinstance(n7_scene_profile, dict):
result["n7_source_scene"] = deepcopy(n7_scene_profile)
result["parsed_control_signatures"] = [
list(self._control_signature(action))
for action in [
@@ -2969,7 +3384,10 @@ class SeatDemoService:
)
)
if (
allow_empty_control_result
(
allow_empty_control_result
or parsed.get("_saved_mode_local_peripheral") is True
)
and not has_control
and not parsed.get("ambiguities")
):
@@ -3019,7 +3437,11 @@ class SeatDemoService:
"hardware mode refuses virtual or unverified vehicle actions: "
+ ", ".join(unsupported)
)
if not parsed.get("actions") and not parsed.get("native_can_actions"):
if (
not parsed.get("actions")
and not parsed.get("native_can_actions")
and not parsed.get("_n7_transfer_actions")
):
raise HardwareSafetyError(
"hardware mode requires at least one verified seat or vehicle CAN operation"
)
@@ -4070,7 +4492,10 @@ class SeatDemoService:
if safety_ambiguities:
return parsed
mode_requests = self.mode_profiles.requests_for_profiles(matched_modes)
matched_profile = matched_modes[0]
mode_requests, n7_transfer_actions = (
self._mode_requests_and_n7_transfers(matched_profile)
)
detected_domains = []
for request in mode_requests:
if request["domain"] not in detected_domains:
@@ -4136,6 +4561,26 @@ class SeatDemoService:
float(mode_parsed.get("confidence", 0)),
float(parsed.get("confidence", 0)),
)
if n7_transfer_actions:
merged["deferred_requests"] = self._merge_unique(
list(merged.get("deferred_requests", [])),
self._n7_transfer_deferred_requests(n7_transfer_actions),
)
merged["_n7_transfer_actions"] = deepcopy(n7_transfer_actions)
merged["_n7_scene_profile"] = {
"id": matched_profile.id,
"name": matched_profile.name,
}
merged["detected_domains"] = self._merge_domains(
list(merged.get("detected_domains", [])),
[
str(action.get("domain") or "")
for action in n7_transfer_actions
if str(action.get("domain") or "")
],
)
if self._has_local_mode_peripheral(matched_profile):
merged["_saved_mode_local_peripheral"] = True
return merged
@staticmethod