feat(n7web): enhance control workflows and status feedback

This commit is contained in:
Miayui
2026-09-16 12:52:07 +08:00
parent 84a3cd73bb
commit 0e4328f548
45 changed files with 7317 additions and 1001 deletions
+123 -23
View File
@@ -43,11 +43,23 @@ from cognitive_agent.application.codex_assistant import ( # noqa: E402
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.seat_service import ( # noqa: E402
LOCAL_CONTROL_BUDGET_MS,
@@ -78,6 +90,11 @@ 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,
@@ -89,7 +106,13 @@ from cognitive_agent.infrastructure.speech import ( # noqa: E402
SpeechTranscriber,
)
STATIC_ROOT = REPOSITORY_ROOT / "apps" / "web-console"
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"
)
@@ -222,6 +245,19 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
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,
@@ -328,6 +364,27 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
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])
@@ -403,6 +460,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
"success": True,
**self.nfc.snapshot(),
"pet_cards": self.pets.snapshot(),
"extension_toy": self.extension_toys.snapshot(),
}
)
return
@@ -885,7 +943,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
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 the NFC-bound peripheral when one is present."""
"""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
@@ -986,7 +1044,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
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 only after the live NFC binding and learned action resolve.
transcript only after the durable NFC binding and learned action resolve.
"""
text = str(utterance or "").strip()
@@ -1328,7 +1386,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
result: dict[str, object],
entries: list[dict[str, object]],
) -> dict[str, object]:
"""Expose command-level NFC denials without discarding allowed work."""
"""Expose disabled bound commands without discarding allowed work."""
if not entries:
return result
@@ -1358,7 +1416,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
for item in denied_actions
)
summaries.append(
f"当前 NFC 未授权 {len(denied_actions)} 条红外命令,已过滤:{labels}。"
f"持久绑定中有 {len(denied_actions)} 条红外能力未启用,已过滤:{labels}。"
)
result["decision_summary"] = summaries
@@ -1487,7 +1545,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
re.search(
r"然后|随后|接着|之后|"
r"(?:\d+(?:\.\d+)?|[一二三四五六七八九十两半]+)"
r"(?:毫秒|秒|分钟)",
r"(?:ms|s|毫秒|秒|分钟)",
residual,
)
)
@@ -1777,7 +1835,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
if isinstance(item, dict)
)
raise ExtensionIntegrationError(
f"当前 NFC 未授权执行以下红外命令:{labels}。"
f"以下持久绑定红外能力未启用,无法执行:{labels}。"
)
result["natural_language_routing"] = deepcopy(natural_language_routing)
@@ -2653,6 +2711,24 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
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(
@@ -2711,18 +2787,6 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
if not operations:
return []
snapshot = self.nfc.snapshot()
card = snapshot.get("card") if isinstance(snapshot, dict) else None
active_uid = re.sub(
r"[^0-9A-Fa-f]",
"",
str((card or {}).get("uid") or ""),
).upper()
if not active_uid or str((card or {}).get("action") or "present") != "present":
raise ExtensionIntegrationError(
"该模式包含 NFC 红外指令,请先放置生成此模式时绑定的 NFC 卡。"
)
catalog = self.extension.list_peripheral_capabilities()
entries: list[dict[str, object]] = []
for index, operation in enumerate(operations):
@@ -2737,6 +2801,15 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
"已保存模式包含无效的红外槽位。"
) 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
@@ -2750,13 +2823,13 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
"",
str(item.get("card_uid") or ""),
).upper()
== active_uid
== bound_uid
),
None,
)
if capability is None:
raise ExtensionIntegrationError(
f"当前 NFC 未授权模式中的红外命令:{device_id} / IR {slot}。"
f"持久绑定目录未授权模式中的红外命令:{device_id} / IR {slot}。"
)
entries.append(
{
@@ -2798,8 +2871,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
utterance=utterance,
source=source,
)
# Validate the complete saved flow, including the live NFC
# binding, before any transport is allowed to execute.
# 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,
@@ -3321,6 +3394,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
**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:
@@ -3963,8 +4038,10 @@ def main() -> None:
)
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", "::"}:
@@ -3995,6 +4072,25 @@ def main() -> None:
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,
@@ -4017,6 +4113,10 @@ def main() -> None:
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()