feat(n7web): enhance control workflows and status feedback
This commit is contained in:
+123
-23
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user