feat: notify vehicle of published firmware
This commit is contained in:
@@ -10,7 +10,7 @@
|
|||||||
<script src="/firmware-workbench-theme.js?v=1"></script>
|
<script src="/firmware-workbench-theme.js?v=1"></script>
|
||||||
<link rel="stylesheet" href="/firmware-workbench.css?v=6" />
|
<link rel="stylesheet" href="/firmware-workbench.css?v=6" />
|
||||||
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
|
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
|
||||||
<script src="/firmware-workbench.js?v=3" defer></script>
|
<script src="/firmware-workbench.js?v=4" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
|
|||||||
@@ -271,6 +271,7 @@
|
|||||||
<button type="button" title="查看验证记录" data-artifact-action="validation" data-artifact-id="${artifact.id}">验证</button>
|
<button type="button" title="查看验证记录" data-artifact-action="validation" data-artifact-id="${artifact.id}">验证</button>
|
||||||
<a href="${escapeHtml(artifact.download_url)}" title="下载固件" aria-label="下载固件" download><i data-lucide="download"></i></a>
|
<a href="${escapeHtml(artifact.download_url)}" title="下载固件" aria-label="下载固件" download><i data-lucide="download"></i></a>
|
||||||
<button class="publish-action" type="button" title="发布到服务器" data-artifact-action="publish" data-artifact-id="${artifact.id}"><i data-lucide="cloud-upload"></i></button>
|
<button class="publish-action" type="button" title="发布到服务器" data-artifact-action="publish" data-artifact-id="${artifact.id}"><i data-lucide="cloud-upload"></i></button>
|
||||||
|
${artifact.status === "published" ? `<button class="notify-action" type="button" title="通知车端" aria-label="通知车端" data-artifact-action="notify" data-artifact-id="${artifact.id}"><i data-lucide="bell"></i></button>` : ""}
|
||||||
${artifact.is_baseline ? `<span class="baseline-protection" title="时间控制基线固件受保护"><i data-lucide="shield-check"></i>受保护</span>` : `<button class="delete-action" type="button" title="删除固件产物" data-artifact-action="delete" data-artifact-id="${artifact.id}"><i data-lucide="trash-2"></i></button>`}
|
${artifact.is_baseline ? `<span class="baseline-protection" title="时间控制基线固件受保护"><i data-lucide="shield-check"></i>受保护</span>` : `<button class="delete-action" type="button" title="删除固件产物" data-artifact-action="delete" data-artifact-id="${artifact.id}"><i data-lucide="trash-2"></i></button>`}
|
||||||
</div></td>
|
</div></td>
|
||||||
</tr>`).join("");
|
</tr>`).join("");
|
||||||
@@ -400,6 +401,16 @@
|
|||||||
finally { button.disabled = false; }
|
finally { button.disabled = false; }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (action === "notify") {
|
||||||
|
button.disabled = true;
|
||||||
|
try {
|
||||||
|
const result = await api(`/api/firmware-workbench/artifacts/${artifact.id}/notify`, { method: "POST", body: "{}" });
|
||||||
|
toast(`已向车端发送 ${result.published_count || result.burst_count || 3} 帧通知。`, "success");
|
||||||
|
await refreshState({ quiet: true });
|
||||||
|
} catch (error) { toast(error.message, "error"); }
|
||||||
|
finally { button.disabled = false; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (action === "detail") {
|
if (action === "detail") {
|
||||||
showModal("固件产物详情", `<div class="detail-grid">
|
showModal("固件产物详情", `<div class="detail-grid">
|
||||||
<div class="detail-field"><span>软件名称</span><strong>${escapeHtml(artifact.software_name)}</strong></div>
|
<div class="detail-field"><span>软件名称</span><strong>${escapeHtml(artifact.software_name)}</strong></div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const CACHE_VERSION = "cognitive-agent-pad-v323-dialog-theme";
|
const CACHE_VERSION = "cognitive-agent-pad-v324-firmware-notification";
|
||||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||||
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
||||||
const APP_SHELL = [
|
const APP_SHELL = [
|
||||||
@@ -28,7 +28,7 @@ const APP_SHELL = [
|
|||||||
"/firmware-workbench.html",
|
"/firmware-workbench.html",
|
||||||
"/firmware-workbench-theme.js?v=1",
|
"/firmware-workbench-theme.js?v=1",
|
||||||
"/firmware-workbench.css?v=6",
|
"/firmware-workbench.css?v=6",
|
||||||
"/firmware-workbench.js?v=3",
|
"/firmware-workbench.js?v=4",
|
||||||
"/assets/pets/self-extension-cat.png?v=3",
|
"/assets/pets/self-extension-cat.png?v=3",
|
||||||
"/assets/pets/self-extension-cat-entrance.png?v=3",
|
"/assets/pets/self-extension-cat-entrance.png?v=3",
|
||||||
"/manifest.webmanifest",
|
"/manifest.webmanifest",
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
import zipfile
|
import zipfile
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable, Mapping
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from cognitive_agent.infrastructure.can.gateway import CanGatewayError
|
||||||
|
|
||||||
PIPELINE_STAGES = (
|
PIPELINE_STAGES = (
|
||||||
("instruction", "指令输入"),
|
("instruction", "指令输入"),
|
||||||
("requirements", "需求解析"),
|
("requirements", "需求解析"),
|
||||||
@@ -41,6 +43,8 @@ BASELINE_CATALOG = {
|
|||||||
SOFTWARE_NAME = "副驾座椅控制器"
|
SOFTWARE_NAME = "副驾座椅控制器"
|
||||||
FIRMWARE_VERSION = "SMV8-r15-b16"
|
FIRMWARE_VERSION = "SMV8-r15-b16"
|
||||||
FINAL_RELEASE_MESSAGE = "已发布,待车机端下载升级"
|
FINAL_RELEASE_MESSAGE = "已发布,待车机端下载升级"
|
||||||
|
FIRMWARE_NOTIFICATION_TOPIC = "from_pc/firmware/notification"
|
||||||
|
FIRMWARE_NOTIFICATION_FRAME_COUNT = 3
|
||||||
|
|
||||||
BuildRunner = Callable[[str, Callable[[str, str], None], threading.Event], Path]
|
BuildRunner = Callable[[str, Callable[[str, str], None], threading.Event], Path]
|
||||||
|
|
||||||
@@ -459,6 +463,113 @@ class FirmwareWorkbenchService:
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
return deepcopy(self._find_artifact_locked(artifact_id))
|
return deepcopy(self._find_artifact_locked(artifact_id))
|
||||||
|
|
||||||
|
def notify_artifact(
|
||||||
|
self,
|
||||||
|
artifact_id: str,
|
||||||
|
publisher: Callable[[Mapping[str, Any]], Mapping[str, Any]],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Send a published artifact notification as one three-frame burst."""
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
artifact = self._find_artifact_locked(artifact_id)
|
||||||
|
if artifact.get("is_baseline") or artifact.get("status") == "baseline":
|
||||||
|
raise FirmwareWorkbenchError(
|
||||||
|
"时间控制基线固件受保护,不能通知车端。",
|
||||||
|
403,
|
||||||
|
"baseline_protected",
|
||||||
|
)
|
||||||
|
if artifact.get("status") != "published":
|
||||||
|
raise FirmwareWorkbenchError(
|
||||||
|
"只有已发布的固件版本才能通知车端。",
|
||||||
|
409,
|
||||||
|
"artifact_not_published",
|
||||||
|
)
|
||||||
|
|
||||||
|
required_text_fields = (
|
||||||
|
"software_name",
|
||||||
|
"version",
|
||||||
|
"release_id",
|
||||||
|
"firmware_filename",
|
||||||
|
"build_time",
|
||||||
|
"sha256",
|
||||||
|
)
|
||||||
|
for field in required_text_fields:
|
||||||
|
value = artifact.get(field)
|
||||||
|
if not isinstance(value, str) or not value.strip():
|
||||||
|
raise FirmwareWorkbenchError(
|
||||||
|
f"已发布固件缺少通知字段:{field}。",
|
||||||
|
409,
|
||||||
|
"artifact_metadata_incomplete",
|
||||||
|
)
|
||||||
|
size_bytes = artifact.get("size_bytes")
|
||||||
|
if isinstance(size_bytes, bool) or not isinstance(size_bytes, int) or size_bytes < 0:
|
||||||
|
raise FirmwareWorkbenchError(
|
||||||
|
"已发布固件的包大小无效。",
|
||||||
|
409,
|
||||||
|
"artifact_metadata_incomplete",
|
||||||
|
)
|
||||||
|
sha256 = str(artifact["sha256"])
|
||||||
|
if len(sha256) != 64 or any(
|
||||||
|
character not in "0123456789abcdefABCDEF" for character in sha256
|
||||||
|
):
|
||||||
|
raise FirmwareWorkbenchError(
|
||||||
|
"已发布固件的 SHA-256 无效。",
|
||||||
|
409,
|
||||||
|
"artifact_metadata_incomplete",
|
||||||
|
)
|
||||||
|
|
||||||
|
message_id = f"fw-notify-{uuid.uuid4().hex[:20]}"
|
||||||
|
sent_at = datetime.now(UTC).isoformat().replace("+00:00", "Z")
|
||||||
|
common_payload: dict[str, Any] = {
|
||||||
|
"schema": "n7.firmware.notification.v1",
|
||||||
|
"event": "firmware_release_available",
|
||||||
|
"message_id": message_id,
|
||||||
|
"sequence_total": FIRMWARE_NOTIFICATION_FRAME_COUNT,
|
||||||
|
"sent_at": sent_at,
|
||||||
|
"source": "n7web",
|
||||||
|
"target": {"ecu": "passenger_seat_controller"},
|
||||||
|
"firmware": {
|
||||||
|
"software_name": artifact["software_name"],
|
||||||
|
"version": artifact["version"],
|
||||||
|
"status": "published",
|
||||||
|
"release_id": artifact["release_id"],
|
||||||
|
"artifact_name": artifact["firmware_filename"],
|
||||||
|
"size_bytes": size_bytes,
|
||||||
|
"sha256": sha256,
|
||||||
|
"build_at": artifact["build_time"],
|
||||||
|
},
|
||||||
|
"action": "notify_only",
|
||||||
|
"auto_upgrade": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
published_count = 0
|
||||||
|
for sequence in range(1, FIRMWARE_NOTIFICATION_FRAME_COUNT + 1):
|
||||||
|
frame = deepcopy(common_payload)
|
||||||
|
frame["sequence"] = sequence
|
||||||
|
try:
|
||||||
|
result = publisher(frame)
|
||||||
|
except CanGatewayError as exc:
|
||||||
|
raise FirmwareWorkbenchError(
|
||||||
|
f"车端通知发送失败,已发送 {published_count}/{FIRMWARE_NOTIFICATION_FRAME_COUNT} 帧:{exc}",
|
||||||
|
503,
|
||||||
|
"notification_publish_failed",
|
||||||
|
) from exc
|
||||||
|
if not isinstance(result, Mapping):
|
||||||
|
raise FirmwareWorkbenchError(
|
||||||
|
f"车端通知发布器返回无效结果,已发送 {published_count}/{FIRMWARE_NOTIFICATION_FRAME_COUNT} 帧。",
|
||||||
|
503,
|
||||||
|
"notification_publish_failed",
|
||||||
|
)
|
||||||
|
published_count += 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
"topic": FIRMWARE_NOTIFICATION_TOPIC,
|
||||||
|
"message_id": message_id,
|
||||||
|
"burst_count": FIRMWARE_NOTIFICATION_FRAME_COUNT,
|
||||||
|
"published_count": published_count,
|
||||||
|
"status": "published",
|
||||||
|
}
|
||||||
|
|
||||||
def publish_artifact(self, artifact_id: str) -> dict[str, Any]:
|
def publish_artifact(self, artifact_id: str) -> dict[str, Any]:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
artifact = self._find_artifact_locked(artifact_id)
|
artifact = self._find_artifact_locked(artifact_id)
|
||||||
|
|||||||
@@ -1807,6 +1807,14 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
|||||||
|
|
||||||
return self._vehicle_mqtt_transport.publish_extension_toy_message(payload)
|
return self._vehicle_mqtt_transport.publish_extension_toy_message(payload)
|
||||||
|
|
||||||
|
def publish_firmware_notification(
|
||||||
|
self,
|
||||||
|
payload: Mapping[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Publish one firmware release notification through the vehicle broker."""
|
||||||
|
|
||||||
|
return self._vehicle_mqtt_transport.publish_firmware_notification(payload)
|
||||||
|
|
||||||
def set_extension_control_handler(
|
def set_extension_control_handler(
|
||||||
self,
|
self,
|
||||||
handler: Callable[[bytes], None] | None,
|
handler: Callable[[bytes], None] | None,
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ MQTT_CAN_SCHEMA = "n7.can.frame.v1"
|
|||||||
MQTT_PET_TOPIC = "n7/pet"
|
MQTT_PET_TOPIC = "n7/pet"
|
||||||
MQTT_EXTENSION_TOY_TOPIC = "extension/toy"
|
MQTT_EXTENSION_TOY_TOPIC = "extension/toy"
|
||||||
MQTT_EXTENSION_TOY_QOS = 1
|
MQTT_EXTENSION_TOY_QOS = 1
|
||||||
|
MQTT_FIRMWARE_NOTIFICATION_TOPIC = "from_pc/firmware/notification"
|
||||||
|
MQTT_FIRMWARE_NOTIFICATION_QOS = 1
|
||||||
MQTT_EXTENSION_CONTROL_TOPIC = "extension/ctl"
|
MQTT_EXTENSION_CONTROL_TOPIC = "extension/ctl"
|
||||||
MQTT_EXTENSION_CONTROL_QOS = 1
|
MQTT_EXTENSION_CONTROL_QOS = 1
|
||||||
MQTT_EXTENSION_CONTROL_MAX_PAYLOAD_BYTES = 256 * 1024
|
MQTT_EXTENSION_CONTROL_MAX_PAYLOAD_BYTES = 256 * 1024
|
||||||
@@ -535,6 +537,114 @@ def mqtt_pet_payload(payload: Mapping[str, Any]) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_FIRMWARE_NOTIFICATION_REQUIRED_KEYS = frozenset(
|
||||||
|
{
|
||||||
|
"schema",
|
||||||
|
"event",
|
||||||
|
"message_id",
|
||||||
|
"sequence",
|
||||||
|
"sequence_total",
|
||||||
|
"sent_at",
|
||||||
|
"source",
|
||||||
|
"target",
|
||||||
|
"firmware",
|
||||||
|
"action",
|
||||||
|
"auto_upgrade",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_FIRMWARE_NOTIFICATION_TARGET_KEYS = frozenset({"ecu"})
|
||||||
|
_FIRMWARE_NOTIFICATION_FIRMWARE_KEYS = frozenset(
|
||||||
|
{
|
||||||
|
"software_name",
|
||||||
|
"version",
|
||||||
|
"status",
|
||||||
|
"release_id",
|
||||||
|
"artifact_name",
|
||||||
|
"size_bytes",
|
||||||
|
"sha256",
|
||||||
|
"build_at",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def mqtt_firmware_notification_payload(payload: Mapping[str, Any]) -> str:
|
||||||
|
"""Serialize one vehicle-facing firmware release notification frame."""
|
||||||
|
|
||||||
|
if not isinstance(payload, Mapping) or set(payload) != _FIRMWARE_NOTIFICATION_REQUIRED_KEYS:
|
||||||
|
raise CanGatewayError(
|
||||||
|
"firmware notification payload requires the agreed top-level fields"
|
||||||
|
)
|
||||||
|
if payload.get("schema") != "n7.firmware.notification.v1":
|
||||||
|
raise CanGatewayError("firmware notification schema is invalid")
|
||||||
|
if payload.get("event") != "firmware_release_available":
|
||||||
|
raise CanGatewayError("firmware notification event is invalid")
|
||||||
|
if payload.get("source") != "n7web":
|
||||||
|
raise CanGatewayError("firmware notification source is invalid")
|
||||||
|
if payload.get("action") != "notify_only" or payload.get("auto_upgrade") is not False:
|
||||||
|
raise CanGatewayError("firmware notification must be notify_only")
|
||||||
|
|
||||||
|
for key, maximum in (("message_id", 128), ("sent_at", 64)):
|
||||||
|
value = payload.get(key)
|
||||||
|
if not isinstance(value, str) or not value.strip() or len(value) > maximum:
|
||||||
|
raise CanGatewayError(f"firmware notification {key} is invalid")
|
||||||
|
for key in ("sequence", "sequence_total"):
|
||||||
|
value = payload.get(key)
|
||||||
|
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
||||||
|
raise CanGatewayError(f"firmware notification {key} must be a positive integer")
|
||||||
|
if payload["sequence_total"] != 3 or payload["sequence"] > payload["sequence_total"]:
|
||||||
|
raise CanGatewayError("firmware notification sequence must be between 1 and 3")
|
||||||
|
|
||||||
|
target = payload.get("target")
|
||||||
|
if not isinstance(target, Mapping) or set(target) != _FIRMWARE_NOTIFICATION_TARGET_KEYS:
|
||||||
|
raise CanGatewayError("firmware notification target must contain only ecu")
|
||||||
|
if (
|
||||||
|
not isinstance(target.get("ecu"), str)
|
||||||
|
or not target["ecu"].strip()
|
||||||
|
or len(target["ecu"]) > 128
|
||||||
|
):
|
||||||
|
raise CanGatewayError("firmware notification target ecu is invalid")
|
||||||
|
|
||||||
|
firmware = payload.get("firmware")
|
||||||
|
if not isinstance(firmware, Mapping) or set(firmware) != _FIRMWARE_NOTIFICATION_FIRMWARE_KEYS:
|
||||||
|
raise CanGatewayError("firmware notification firmware fields are invalid")
|
||||||
|
for key, maximum in (
|
||||||
|
("software_name", 128),
|
||||||
|
("version", 128),
|
||||||
|
("status", 32),
|
||||||
|
("release_id", 128),
|
||||||
|
("artifact_name", 255),
|
||||||
|
("build_at", 64),
|
||||||
|
):
|
||||||
|
value = firmware.get(key)
|
||||||
|
if not isinstance(value, str) or not value.strip() or len(value) > maximum:
|
||||||
|
raise CanGatewayError(f"firmware notification firmware {key} is invalid")
|
||||||
|
if firmware.get("status") != "published":
|
||||||
|
raise CanGatewayError("firmware notification requires a published firmware")
|
||||||
|
size_bytes = firmware.get("size_bytes")
|
||||||
|
if isinstance(size_bytes, bool) or not isinstance(size_bytes, int) or size_bytes < 0:
|
||||||
|
raise CanGatewayError("firmware notification size_bytes is invalid")
|
||||||
|
sha256 = firmware.get("sha256")
|
||||||
|
if (
|
||||||
|
not isinstance(sha256, str)
|
||||||
|
or len(sha256) != 64
|
||||||
|
or any(character not in "0123456789abcdefABCDEF" for character in sha256)
|
||||||
|
):
|
||||||
|
raise CanGatewayError("firmware notification sha256 must be a 64-character hexadecimal hash")
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw_payload = json.dumps(
|
||||||
|
deepcopy(dict(payload)),
|
||||||
|
ensure_ascii=False,
|
||||||
|
separators=(",", ":"),
|
||||||
|
allow_nan=False,
|
||||||
|
)
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise CanGatewayError("firmware notification payload is not valid JSON") from exc
|
||||||
|
if len(raw_payload.encode("utf-8")) > 16 * 1024:
|
||||||
|
raise CanGatewayError("firmware notification payload exceeds 16 KiB")
|
||||||
|
return raw_payload
|
||||||
|
|
||||||
|
|
||||||
_EXTENSION_TOY_ACTION_KEYS = frozenset(
|
_EXTENSION_TOY_ACTION_KEYS = frozenset(
|
||||||
{
|
{
|
||||||
"type",
|
"type",
|
||||||
@@ -1512,6 +1622,38 @@ class MqttCanTransport:
|
|||||||
"message_id": getattr(result, "mid", None),
|
"message_id": getattr(result, "mid", None),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def publish_firmware_notification(
|
||||||
|
self,
|
||||||
|
payload: Mapping[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Queue one validated firmware release notification frame."""
|
||||||
|
|
||||||
|
raw_payload = mqtt_firmware_notification_payload(payload)
|
||||||
|
with self._condition:
|
||||||
|
client = self._client
|
||||||
|
state = self._state
|
||||||
|
if client is None or state != "online":
|
||||||
|
raise CanGatewayError("firmware notification MQTT publisher is not connected")
|
||||||
|
result = client.publish(
|
||||||
|
MQTT_FIRMWARE_NOTIFICATION_TOPIC,
|
||||||
|
raw_payload,
|
||||||
|
qos=MQTT_FIRMWARE_NOTIFICATION_QOS,
|
||||||
|
retain=False,
|
||||||
|
)
|
||||||
|
if int(getattr(result, "rc", 0)) != 0:
|
||||||
|
raise CanGatewayError(
|
||||||
|
f"firmware notification MQTT publish failed with code {result.rc}"
|
||||||
|
)
|
||||||
|
is_published = getattr(result, "is_published", None)
|
||||||
|
broker_confirmed = bool(is_published()) if callable(is_published) else False
|
||||||
|
return {
|
||||||
|
"topic": MQTT_FIRMWARE_NOTIFICATION_TOPIC,
|
||||||
|
"qos": MQTT_FIRMWARE_NOTIFICATION_QOS,
|
||||||
|
"retain": False,
|
||||||
|
"delivery_status": "published" if broker_confirmed else "queued",
|
||||||
|
"message_id": getattr(result, "mid", None),
|
||||||
|
}
|
||||||
|
|
||||||
def publish_extension_toy_message(
|
def publish_extension_toy_message(
|
||||||
self,
|
self,
|
||||||
payload: Mapping[str, Any],
|
payload: Mapping[str, Any],
|
||||||
|
|||||||
@@ -3878,6 +3878,18 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
artifact_notify_match = re.fullmatch(
|
||||||
|
r"/api/firmware-workbench/artifacts/(?P<artifact_id>[a-z0-9-]+)/notify",
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
if artifact_notify_match:
|
||||||
|
self._json(
|
||||||
|
self.firmware_workbench.notify_artifact(
|
||||||
|
artifact_notify_match.group("artifact_id"),
|
||||||
|
self.service.publish_firmware_notification,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
|
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
|
||||||
except (ValueError, json.JSONDecodeError) as exc:
|
except (ValueError, json.JSONDecodeError) as exc:
|
||||||
self._json(
|
self._json(
|
||||||
@@ -3886,6 +3898,11 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
)
|
)
|
||||||
except FirmwareWorkbenchError as exc:
|
except FirmwareWorkbenchError as exc:
|
||||||
self._json({"error": exc.code, "message": str(exc)}, HTTPStatus(exc.status))
|
self._json({"error": exc.code, "message": str(exc)}, HTTPStatus(exc.status))
|
||||||
|
except CanGatewayError as exc:
|
||||||
|
self._json(
|
||||||
|
{"error": "notification_publish_failed", "message": str(exc)},
|
||||||
|
HTTPStatus.SERVICE_UNAVAILABLE,
|
||||||
|
)
|
||||||
|
|
||||||
def _serve_firmware_download(self, kind: str, item_id: str) -> None:
|
def _serve_firmware_download(self, kind: str, item_id: str) -> None:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1385,7 +1385,7 @@ operations:
|
|||||||
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
|
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
|
||||||
self.assertIn('"cognitive-agent:module-change"', app_script)
|
self.assertIn('"cognitive-agent:module-change"', app_script)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
'const CACHE_VERSION = "cognitive-agent-pad-v323-dialog-theme"',
|
'const CACHE_VERSION = "cognitive-agent-pad-v324-firmware-notification"',
|
||||||
service_worker,
|
service_worker,
|
||||||
)
|
)
|
||||||
self.assertIn('/console-v2.css?v=delivery-state-69', service_worker)
|
self.assertIn('/console-v2.css?v=delivery-state-69', service_worker)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ def test_console_contains_ai_assistant_surface():
|
|||||||
def test_codex_assistant_assets_are_versioned_together_in_the_service_worker():
|
def test_codex_assistant_assets_are_versioned_together_in_the_service_worker():
|
||||||
service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8")
|
service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8")
|
||||||
assert (
|
assert (
|
||||||
'const CACHE_VERSION = "cognitive-agent-pad-v323-dialog-theme"'
|
'const CACHE_VERSION = "cognitive-agent-pad-v324-firmware-notification"'
|
||||||
in service_worker
|
in service_worker
|
||||||
)
|
)
|
||||||
assert '"/assistant-chat.css?v=codex-assistant-29"' in service_worker
|
assert '"/assistant-chat.css?v=codex-assistant-29"' in service_worker
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ def test_energy_monitor_assets_are_versioned_and_cached() -> None:
|
|||||||
"/energy-monitor.js?v=energy-monitor-5-performance"
|
"/energy-monitor.js?v=energy-monitor-5-performance"
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
'const CACHE_VERSION = "cognitive-agent-pad-v323-dialog-theme"'
|
'const CACHE_VERSION = "cognitive-agent-pad-v324-firmware-notification"'
|
||||||
in service_worker
|
in service_worker
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ def test_workbench_exposes_artifact_actions_and_always_starts_a_new_task() -> No
|
|||||||
"/firmware-workbench.html",
|
"/firmware-workbench.html",
|
||||||
"/firmware-workbench-theme.js?v=1",
|
"/firmware-workbench-theme.js?v=1",
|
||||||
"/firmware-workbench.css?v=6",
|
"/firmware-workbench.css?v=6",
|
||||||
"/firmware-workbench.js?v=3",
|
"/firmware-workbench.js?v=4",
|
||||||
):
|
):
|
||||||
assert f'"{asset}"' in worker
|
assert f'"{asset}"' in worker
|
||||||
for runtime_file in (
|
for runtime_file in (
|
||||||
|
|||||||
@@ -99,5 +99,5 @@ def test_performance_assets_have_a_new_cache_generation() -> None:
|
|||||||
):
|
):
|
||||||
assert asset in html
|
assert asset in html
|
||||||
assert f'"{asset}"' in worker
|
assert f'"{asset}"' in worker
|
||||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v323-dialog-theme"' in worker
|
assert 'const CACHE_VERSION = "cognitive-agent-pad-v324-firmware-notification"' in worker
|
||||||
assert '"/codepen-glass-theme.css?v=creative-cloud-6-performance"' in worker
|
assert '"/codepen-glass-theme.css?v=creative-cloud-6-performance"' in worker
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None:
|
|||||||
assert source_asset in main
|
assert source_asset in main
|
||||||
assert f'"{cached_asset}"' in service_worker
|
assert f'"{cached_asset}"' in service_worker
|
||||||
assert 'import "./styles/design-system.css"' in main
|
assert 'import "./styles/design-system.css"' in main
|
||||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v323-dialog-theme"' in service_worker
|
assert 'const CACHE_VERSION = "cognitive-agent-pad-v324-firmware-notification"' in service_worker
|
||||||
|
|
||||||
|
|
||||||
def test_reference_glass_theme_covers_the_complete_workspace() -> None:
|
def test_reference_glass_theme_covers_the_complete_workspace() -> None:
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ def test_vue_vite_application_is_the_primary_frontend_entry() -> None:
|
|||||||
assert package["scripts"]["build"] == "vite build"
|
assert package["scripts"]["build"] == "vite build"
|
||||||
assert 'import vue from "@vitejs/plugin-vue"' in vite
|
assert 'import vue from "@vitejs/plugin-vue"' in vite
|
||||||
assert 'outDir: outputRoot' in vite
|
assert 'outDir: outputRoot' in vite
|
||||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v323-dialog-theme"' in service_worker
|
assert 'const CACHE_VERSION = "cognitive-agent-pad-v324-firmware-notification"' in service_worker
|
||||||
assert "unpkg.com" not in html
|
assert "unpkg.com" not in html
|
||||||
assert "cdn.jsdelivr.net" not in html
|
assert "cdn.jsdelivr.net" not in html
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user