feat(web): add peripheral card quick controls
This commit is contained in:
@@ -44,7 +44,7 @@
|
|||||||
<script type="module" src="/gesture-control.js?v=android-pad-9"></script>
|
<script type="module" src="/gesture-control.js?v=android-pad-9"></script>
|
||||||
<script src="/pwa.js?v=android-pad-3" defer></script>
|
<script src="/pwa.js?v=android-pad-3" defer></script>
|
||||||
<script src="/console-v2.js?v=local-first-79-cinema-status" defer></script>
|
<script src="/console-v2.js?v=local-first-79-cinema-status" defer></script>
|
||||||
<script src="/self-extension.js?v=nfc-session-58" defer></script>
|
<script src="/self-extension.js?v=peripheral-cards-59" defer></script>
|
||||||
<script src="/assistant-chat.js?v=codex-assistant-39-video-media" defer></script>
|
<script src="/assistant-chat.js?v=codex-assistant-39-video-media" defer></script>
|
||||||
<script src="/energy-monitor.js?v=energy-monitor-5-performance" defer></script>
|
<script src="/energy-monitor.js?v=energy-monitor-5-performance" defer></script>
|
||||||
<script src="/firmware-workbench-entry.js?v=3" defer></script>
|
<script src="/firmware-workbench-entry.js?v=3" defer></script>
|
||||||
|
|||||||
@@ -2698,6 +2698,146 @@ form > .v2-extension-primary {
|
|||||||
font-size: 7px;
|
font-size: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v2-extension-synced-device {
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid rgba(107, 214, 203, 0.12);
|
||||||
|
transition: border-color 160ms ease, background 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-synced-device[data-expanded="true"] {
|
||||||
|
border-color: rgba(107, 214, 203, 0.3);
|
||||||
|
background: rgba(87, 191, 180, 0.075);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-synced-device > header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 7px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-device-toggle {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 28px minmax(0, 1fr) auto 14px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-device-toggle > span {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: #76cfc6;
|
||||||
|
background: rgba(87, 191, 180, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-device-toggle > span svg,
|
||||||
|
.v2-extension-device-toggle > svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-device-toggle > div {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-device-toggle > div strong {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-device-toggle > b {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-device-toggle > svg {
|
||||||
|
color: #8da7a5;
|
||||||
|
transition: transform 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-synced-device[data-expanded="true"] .v2-extension-device-toggle > svg {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-device-hide {
|
||||||
|
width: 27px;
|
||||||
|
height: 27px;
|
||||||
|
flex: none;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0;
|
||||||
|
color: #8da7a5;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 7px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-device-hide:is(:hover, :focus-visible) {
|
||||||
|
color: #eaa89c;
|
||||||
|
background: rgba(216, 99, 82, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-device-hide svg {
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-synced-device > .v2-extension-quick-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 8px 8px 44px;
|
||||||
|
border-top: 1px solid rgba(107, 214, 203, 0.09);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-quick-controls > button {
|
||||||
|
min-height: 25px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
color: #b9d8d4;
|
||||||
|
background: rgba(95, 190, 180, 0.1);
|
||||||
|
border: 1px solid rgba(107, 214, 203, 0.13);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 7px;
|
||||||
|
font-weight: 800;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-quick-controls > button:is(:hover, :focus-visible) {
|
||||||
|
color: #effaf8;
|
||||||
|
background: rgba(95, 190, 180, 0.2);
|
||||||
|
border-color: rgba(107, 214, 203, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-quick-controls > button:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-quick-controls > button[data-state="success"] {
|
||||||
|
color: #bdf2d5;
|
||||||
|
background: rgba(61, 173, 112, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-extension-quick-controls > button[data-state="error"] {
|
||||||
|
color: #f0b4aa;
|
||||||
|
background: rgba(216, 99, 82, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
.v2-extension-sync-mount .v2-extension-empty {
|
.v2-extension-sync-mount .v2-extension-empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -3547,6 +3687,37 @@ form > .v2-extension-primary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-device-toggle > span {
|
||||||
|
color: var(--primary);
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-device-toggle > svg,
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-device-hide {
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-synced-device[data-expanded="true"] {
|
||||||
|
background: color-mix(in oklch, var(--accent) 64%, var(--card));
|
||||||
|
border-color: color-mix(in oklch, var(--primary) 30%, var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-synced-device > .v2-extension-quick-controls {
|
||||||
|
border-top-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-quick-controls > button {
|
||||||
|
color: var(--primary);
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: color-mix(in oklch, var(--primary) 18%, var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-v2 #v2ExtensionPanel .v2-extension-quick-controls > button:is(:hover, :focus-visible) {
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
background: var(--primary);
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
/* Final semantic theme layer for extension surfaces and nested dialogs --- */
|
/* Final semantic theme layer for extension surfaces and nested dialogs --- */
|
||||||
.console-v2 #v2ExtensionPanel .v2-extension-sync-mount,
|
.console-v2 #v2ExtensionPanel .v2-extension-sync-mount,
|
||||||
.console-v2 #v2ExtensionPanel .v2-extension-gateway,
|
.console-v2 #v2ExtensionPanel .v2-extension-gateway,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
const SLOT_CATALOG_MIGRATION_KEY = "cognitive-agent.gscu-slot-catalog-migrated.v1";
|
const SLOT_CATALOG_MIGRATION_KEY = "cognitive-agent.gscu-slot-catalog-migrated.v1";
|
||||||
const MODE_KEY = "cognitive-agent.extension-mode";
|
const MODE_KEY = "cognitive-agent.extension-mode";
|
||||||
const CONVERSATION_KEY = "cognitive-agent.conversation-id";
|
const CONVERSATION_KEY = "cognitive-agent.conversation-id";
|
||||||
|
const PERIPHERAL_CARD_UI_KEY = "cognitive-agent.extension-peripheral-cards.v1";
|
||||||
const SLOT_MIN = 1;
|
const SLOT_MIN = 1;
|
||||||
const SLOT_MAX = 248;
|
const SLOT_MAX = 248;
|
||||||
const ACTION_PAGE_SIZE = 6;
|
const ACTION_PAGE_SIZE = 6;
|
||||||
@@ -375,10 +376,13 @@
|
|||||||
};
|
};
|
||||||
let extensionToySnapshot = {
|
let extensionToySnapshot = {
|
||||||
revision: -1,
|
revision: -1,
|
||||||
|
session_id: null,
|
||||||
state: "stopped",
|
state: "stopped",
|
||||||
topic: "extension/toy",
|
topic: "extension/toy",
|
||||||
burst_published_count: 0,
|
burst_published_count: 0,
|
||||||
detected_nfc_ids: [],
|
detected_nfc_ids: [],
|
||||||
|
detection_count: 0,
|
||||||
|
active_nfc_id: null,
|
||||||
last_error: null,
|
last_error: null,
|
||||||
};
|
};
|
||||||
let peripheralCatalogRevision = null;
|
let peripheralCatalogRevision = null;
|
||||||
@@ -410,6 +414,10 @@
|
|||||||
let legacyMigrationCompleted = false;
|
let legacyMigrationCompleted = false;
|
||||||
const legacyBridgeAttempts = new Set();
|
const legacyBridgeAttempts = new Set();
|
||||||
let renderUnifiedExtensionView = null;
|
let renderUnifiedExtensionView = null;
|
||||||
|
const hiddenPeripheralDevices = new Map();
|
||||||
|
let expandedPeripheralDeviceId = null;
|
||||||
|
let peripheralCardSessionId = "";
|
||||||
|
let runningPeripheralQuickAction = false;
|
||||||
let latestVehicleStateTurn = -1;
|
let latestVehicleStateTurn = -1;
|
||||||
let sharedTwinBand = null;
|
let sharedTwinBand = null;
|
||||||
let sharedTwinHome = null;
|
let sharedTwinHome = null;
|
||||||
@@ -1494,6 +1502,59 @@
|
|||||||
return String(value || "").replace(/[^0-9a-f]/gi, "").toUpperCase();
|
return String(value || "").replace(/[^0-9a-f]/gi, "").toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function persistPeripheralCardUi() {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(PERIPHERAL_CARD_UI_KEY, JSON.stringify({
|
||||||
|
sessionId: peripheralCardSessionId,
|
||||||
|
hidden: [...hiddenPeripheralDevices].map(([deviceId, value]) => ({
|
||||||
|
deviceId,
|
||||||
|
cardUid: value.cardUid,
|
||||||
|
detectionCount: value.detectionCount,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
} catch (_error) {
|
||||||
|
// The in-memory state remains authoritative when storage is unavailable.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function restorePeripheralCardUi(sessionId) {
|
||||||
|
hiddenPeripheralDevices.clear();
|
||||||
|
expandedPeripheralDeviceId = null;
|
||||||
|
peripheralCardSessionId = String(sessionId || "");
|
||||||
|
try {
|
||||||
|
const saved = JSON.parse(sessionStorage.getItem(PERIPHERAL_CARD_UI_KEY) || "null");
|
||||||
|
if (!saved || saved.sessionId !== peripheralCardSessionId) {
|
||||||
|
persistPeripheralCardUi();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(Array.isArray(saved.hidden) ? saved.hidden : []).forEach((item) => {
|
||||||
|
const deviceId = String(item?.deviceId || "").trim();
|
||||||
|
const cardUid = normalizeCardUid(item?.cardUid);
|
||||||
|
const detectionCount = Number(item?.detectionCount ?? -1);
|
||||||
|
if (deviceId && cardUid && Number.isInteger(detectionCount)) {
|
||||||
|
hiddenPeripheralDevices.set(deviceId, { cardUid, detectionCount });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (_error) {
|
||||||
|
persistPeripheralCardUi();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function revealRedetectedPeripheral(value) {
|
||||||
|
const cardUid = normalizeCardUid(value?.active_nfc_id);
|
||||||
|
const detectionCount = Number(value?.detection_count ?? -1);
|
||||||
|
if (!cardUid || !Number.isInteger(detectionCount)) return false;
|
||||||
|
let changed = false;
|
||||||
|
hiddenPeripheralDevices.forEach((hidden, deviceId) => {
|
||||||
|
if (hidden.cardUid === cardUid && hidden.detectionCount !== detectionCount) {
|
||||||
|
hiddenPeripheralDevices.delete(deviceId);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (changed) persistPeripheralCardUi();
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
function renderMetrics() {
|
function renderMetrics() {
|
||||||
$("#extensionHardwareCount").textContent = String(state.devices.length);
|
$("#extensionHardwareCount").textContent = String(state.devices.length);
|
||||||
$("#extensionCapabilityCount").textContent = String(learnedCapabilities().length);
|
$("#extensionCapabilityCount").textContent = String(learnedCapabilities().length);
|
||||||
@@ -4007,9 +4068,16 @@
|
|||||||
function applyExtensionToySnapshot(value, { announce = true } = {}) {
|
function applyExtensionToySnapshot(value, { announce = true } = {}) {
|
||||||
if (!value || typeof value !== "object") return false;
|
if (!value || typeof value !== "object") return false;
|
||||||
const revision = Number(value.revision ?? -1);
|
const revision = Number(value.revision ?? -1);
|
||||||
if (revision === extensionToySnapshot.revision) return false;
|
const sessionId = String(value.session_id || "");
|
||||||
|
const sessionChanged = sessionId !== peripheralCardSessionId;
|
||||||
|
if (sessionChanged) restorePeripheralCardUi(sessionId);
|
||||||
|
const cardRevealed = revealRedetectedPeripheral(value);
|
||||||
|
if (revision === extensionToySnapshot.revision
|
||||||
|
&& sessionId === String(extensionToySnapshot.session_id || "")
|
||||||
|
&& !cardRevealed) return false;
|
||||||
extensionToySnapshot = {
|
extensionToySnapshot = {
|
||||||
revision,
|
revision,
|
||||||
|
session_id: sessionId || null,
|
||||||
state: String(value.state || "stopped"),
|
state: String(value.state || "stopped"),
|
||||||
topic: String(value.topic || "extension/toy"),
|
topic: String(value.topic || "extension/toy"),
|
||||||
burst_published_count: Number(value.burst_published_count || 0),
|
burst_published_count: Number(value.burst_published_count || 0),
|
||||||
@@ -4019,9 +4087,12 @@
|
|||||||
detected_nfc_ids: Array.isArray(value.detected_nfc_ids)
|
detected_nfc_ids: Array.isArray(value.detected_nfc_ids)
|
||||||
? value.detected_nfc_ids.map(normalizeCardUid).filter(Boolean)
|
? value.detected_nfc_ids.map(normalizeCardUid).filter(Boolean)
|
||||||
: [],
|
: [],
|
||||||
|
detection_count: Number(value.detection_count || 0),
|
||||||
|
active_nfc_id: normalizeCardUid(value.active_nfc_id) || null,
|
||||||
last_delivery_status: value.last_delivery_status || null,
|
last_delivery_status: value.last_delivery_status || null,
|
||||||
last_error: value.last_error || null,
|
last_error: value.last_error || null,
|
||||||
};
|
};
|
||||||
|
if (sessionChanged || cardRevealed) unifiedExtensionDirty = true;
|
||||||
if (announce && extensionToySnapshot.state === "burst_complete") {
|
if (announce && extensionToySnapshot.state === "burst_complete") {
|
||||||
showToast(
|
showToast(
|
||||||
`已向 ${extensionToySnapshot.topic} 发送 ${extensionToySnapshot.burst_published_count} 帧设备能力消息。`,
|
`已向 ${extensionToySnapshot.topic} 发送 ${extensionToySnapshot.burst_published_count} 帧设备能力消息。`,
|
||||||
@@ -4119,8 +4190,107 @@
|
|||||||
void refreshNfcStatus();
|
void refreshNfcStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hidePeripheralCard(deviceId, cardUid) {
|
||||||
|
const normalizedDeviceId = String(deviceId || "").trim();
|
||||||
|
const normalizedCardUid = normalizeCardUid(cardUid);
|
||||||
|
if (!normalizedDeviceId || !normalizedCardUid) return;
|
||||||
|
hiddenPeripheralDevices.set(normalizedDeviceId, {
|
||||||
|
cardUid: normalizedCardUid,
|
||||||
|
detectionCount: Number(extensionToySnapshot.detection_count || 0),
|
||||||
|
});
|
||||||
|
if (expandedPeripheralDeviceId === normalizedDeviceId) {
|
||||||
|
expandedPeripheralDeviceId = null;
|
||||||
|
}
|
||||||
|
persistPeripheralCardUi();
|
||||||
|
unifiedExtensionDirty = true;
|
||||||
|
renderUnifiedExtensionView?.({ force: true });
|
||||||
|
showToast("外设卡片已隐藏;再次感应对应 NFC 后将重新显示。", "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePeripheralCard(deviceId) {
|
||||||
|
const normalizedDeviceId = String(deviceId || "").trim();
|
||||||
|
if (!normalizedDeviceId) return;
|
||||||
|
expandedPeripheralDeviceId = expandedPeripheralDeviceId === normalizedDeviceId
|
||||||
|
? null
|
||||||
|
: normalizedDeviceId;
|
||||||
|
unifiedExtensionDirty = true;
|
||||||
|
renderUnifiedExtensionView?.({ force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runPeripheralQuickAction(button) {
|
||||||
|
const capabilityId = String(button?.dataset.runPeripheralQuickAction || "").trim();
|
||||||
|
if (!capabilityId || button.disabled) return;
|
||||||
|
if (runningPeripheralQuickAction) {
|
||||||
|
showToast("上一条外设快捷指令仍在处理,请稍候。", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const capability = durablePeripheralCatalog.find(
|
||||||
|
(item) => String(item.id || "") === capabilityId,
|
||||||
|
);
|
||||||
|
const deviceName = String(
|
||||||
|
capability?.device_name || capability?.device_id || "外设",
|
||||||
|
);
|
||||||
|
const actionName = String(capability?.action || capability?.name || "动作");
|
||||||
|
if ((button.dataset.risk || capability?.risk) === "high" && !window.confirm(
|
||||||
|
`“${deviceName} · ${actionName}”可能让外设移动,请确认周围安全。`,
|
||||||
|
)) return;
|
||||||
|
const previousText = button.textContent;
|
||||||
|
runningPeripheralQuickAction = true;
|
||||||
|
button.disabled = true;
|
||||||
|
button.dataset.state = "running";
|
||||||
|
button.textContent = "下发中…";
|
||||||
|
try {
|
||||||
|
await api("peripherals/execute", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
capability_id: capabilityId,
|
||||||
|
conversation_id: sharedConversationId(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
button.dataset.state = "success";
|
||||||
|
button.textContent = "已下发";
|
||||||
|
showToast(`${deviceName} · ${actionName} 已下发。`, "success");
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (!button.isConnected) return;
|
||||||
|
button.dataset.state = "";
|
||||||
|
button.textContent = previousText;
|
||||||
|
button.disabled = false;
|
||||||
|
}, 1200);
|
||||||
|
} catch (error) {
|
||||||
|
button.dataset.state = "error";
|
||||||
|
button.textContent = "下发失败";
|
||||||
|
showToast(error.message, "error");
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (!button.isConnected) return;
|
||||||
|
button.dataset.state = "";
|
||||||
|
button.textContent = previousText;
|
||||||
|
button.disabled = false;
|
||||||
|
}, 1600);
|
||||||
|
} finally {
|
||||||
|
runningPeripheralQuickAction = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function bindUnifiedNfcEvents(mount) {
|
function bindUnifiedNfcEvents(mount) {
|
||||||
mount.addEventListener("click", (event) => {
|
mount.addEventListener("click", (event) => {
|
||||||
|
const quickAction = event.target.closest("[data-run-peripheral-quick-action]");
|
||||||
|
if (quickAction) {
|
||||||
|
void runPeripheralQuickAction(quickAction);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hideCard = event.target.closest("[data-hide-peripheral-card]");
|
||||||
|
if (hideCard) {
|
||||||
|
hidePeripheralCard(
|
||||||
|
hideCard.dataset.hidePeripheralCard,
|
||||||
|
hideCard.dataset.cardUid,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const toggleCard = event.target.closest("[data-toggle-peripheral-card]");
|
||||||
|
if (toggleCard) {
|
||||||
|
togglePeripheralCard(toggleCard.dataset.togglePeripheralCard);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.target.closest("[data-open-pet-settings]")) openPetSettingsDialog();
|
if (event.target.closest("[data-open-pet-settings]")) openPetSettingsDialog();
|
||||||
else if (event.target.closest("[data-open-nfc-binding]")) openNfcDialog();
|
else if (event.target.closest("[data-open-nfc-binding]")) openNfcDialog();
|
||||||
else if (event.target.closest("[data-open-gscu-tcp]")) openGscuTcpDialog();
|
else if (event.target.closest("[data-open-gscu-tcp]")) openGscuTcpDialog();
|
||||||
@@ -4237,6 +4407,11 @@
|
|||||||
card: nfcSnapshot.card,
|
card: nfcSnapshot.card,
|
||||||
},
|
},
|
||||||
detectedExtensionNfcIds: extensionToySnapshot.detected_nfc_ids,
|
detectedExtensionNfcIds: extensionToySnapshot.detected_nfc_ids,
|
||||||
|
peripheralCardUi: {
|
||||||
|
sessionId: peripheralCardSessionId,
|
||||||
|
hiddenDeviceIds: [...hiddenPeripheralDevices.keys()].sort(),
|
||||||
|
expandedDeviceId: expandedPeripheralDeviceId,
|
||||||
|
},
|
||||||
pets: petCardSnapshot,
|
pets: petCardSnapshot,
|
||||||
localDevices: state.devices.map((device) => ({
|
localDevices: state.devices.map((device) => ({
|
||||||
id: device.id,
|
id: device.id,
|
||||||
@@ -4335,14 +4510,23 @@
|
|||||||
actions: [],
|
actions: [],
|
||||||
});
|
});
|
||||||
const actionName = item.action || item.name || "已学习能力";
|
const actionName = item.action || item.name || "已学习能力";
|
||||||
grouped.get(deviceId).actions.push(
|
grouped.get(deviceId).actions.push({
|
||||||
item.enabled === false ? `${actionName}(已停用)` : actionName,
|
id: String(item.id || ""),
|
||||||
);
|
name: actionName,
|
||||||
|
enabled: item.enabled !== false,
|
||||||
|
risk: item.risk || "low",
|
||||||
|
slot: item.slot,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
const detectedNfcIds = new Set(extensionToySnapshot.detected_nfc_ids);
|
const detectedNfcIds = new Set(extensionToySnapshot.detected_nfc_ids);
|
||||||
const devices = [...grouped.values()].filter(
|
const devices = [...grouped.values()].filter(
|
||||||
(device) => detectedNfcIds.has(normalizeCardUid(device.cardUid)),
|
(device) => detectedNfcIds.has(normalizeCardUid(device.cardUid))
|
||||||
|
&& !hiddenPeripheralDevices.has(device.id),
|
||||||
);
|
);
|
||||||
|
if (expandedPeripheralDeviceId
|
||||||
|
&& !devices.some((device) => device.id === expandedPeripheralDeviceId)) {
|
||||||
|
expandedPeripheralDeviceId = null;
|
||||||
|
}
|
||||||
const lastAction = snapshot?.extension?.last_action;
|
const lastAction = snapshot?.extension?.last_action;
|
||||||
const lastResult = snapshot?.extension?.result;
|
const lastResult = snapshot?.extension?.result;
|
||||||
const nfcCapabilities = nfcDevice
|
const nfcCapabilities = nfcDevice
|
||||||
@@ -4419,11 +4603,24 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="v2-extension-device-list">
|
<div class="v2-extension-device-list">
|
||||||
${nfcPet ? renderDetectedPetCard(nfcPet) : ""}
|
${nfcPet ? renderDetectedPetCard(nfcPet) : ""}
|
||||||
${devices.length ? devices.map((device) => `
|
${devices.length ? devices.map((device) => {
|
||||||
<article class="v2-extension-synced-device">
|
const expanded = expandedPeripheralDeviceId === device.id;
|
||||||
<header><span><i data-lucide="${device.cardUid ? "badge-check" : "cpu"}"></i></span><div><strong title="${escapeHtml(device.type || device.name)}">${escapeHtml(device.name)}</strong></div><b>${device.actions.length} 项能力</b></header>
|
return `
|
||||||
<div>${device.actions.slice(0, 8).map((action) => `<span>${escapeHtml(action)}</span>`).join("")}</div>
|
<article class="v2-extension-synced-device" data-expanded="${String(expanded)}" data-extension-synced-device="${escapeHtml(device.id)}">
|
||||||
</article>`).join("") : nfcPet ? "" : `
|
<header>
|
||||||
|
<button class="v2-extension-device-toggle" type="button" data-toggle-peripheral-card="${escapeHtml(device.id)}" aria-expanded="${String(expanded)}">
|
||||||
|
<span><i data-lucide="${device.cardUid ? "badge-check" : "cpu"}"></i></span>
|
||||||
|
<div><strong title="${escapeHtml(device.type || device.name)}">${escapeHtml(device.name)}</strong></div>
|
||||||
|
<b>${device.actions.length} 项能力</b>
|
||||||
|
<i data-lucide="chevron-down"></i>
|
||||||
|
</button>
|
||||||
|
<button class="v2-extension-device-hide" type="button" data-hide-peripheral-card="${escapeHtml(device.id)}" data-card-uid="${escapeHtml(device.cardUid)}" title="隐藏${escapeHtml(device.name)}" aria-label="隐藏${escapeHtml(device.name)}"><i data-lucide="eye-off"></i></button>
|
||||||
|
</header>
|
||||||
|
${expanded ? `<div class="v2-extension-quick-controls" aria-label="${escapeHtml(device.name)}快捷控制">
|
||||||
|
${device.actions.map((action) => `<button type="button" data-run-peripheral-quick-action="${escapeHtml(action.id)}" data-risk="${escapeHtml(action.risk)}" ${action.enabled && action.id ? "" : "disabled"}>${escapeHtml(action.name)}${action.enabled ? "" : "(已停用)"}</button>`).join("")}
|
||||||
|
</div>` : ""}
|
||||||
|
</article>`;
|
||||||
|
}).join("") : nfcPet ? "" : `
|
||||||
<div class="v2-extension-empty">
|
<div class="v2-extension-empty">
|
||||||
<i data-lucide="scan-line"></i><div><strong>等待 NFC 外设靠近</strong><span>检测到已绑定的 NFC 后,外设将在本次运行期间持续显示</span></div>
|
<i data-lucide="scan-line"></i><div><strong>等待 NFC 外设靠近</strong><span>检测到已绑定的 NFC 后,外设将在本次运行期间持续显示</span></div>
|
||||||
</div>`}
|
</div>`}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ const APP_SHELL = [
|
|||||||
"/pwa.js?v=android-pad-3",
|
"/pwa.js?v=android-pad-3",
|
||||||
"/console-v2.css?v=delivery-state-69",
|
"/console-v2.css?v=delivery-state-69",
|
||||||
"/console-v2.js?v=local-first-79-cinema-status",
|
"/console-v2.js?v=local-first-79-cinema-status",
|
||||||
"/self-extension.css?v=pet-settings-54-dialog-size",
|
"/self-extension.css?v=peripheral-cards-55",
|
||||||
"/self-extension.js?v=nfc-session-58",
|
"/self-extension.js?v=peripheral-cards-59",
|
||||||
"/speech-input.css?v=sensevoice-7",
|
"/speech-input.css?v=sensevoice-7",
|
||||||
"/speech-input.js?v=sensevoice-13",
|
"/speech-input.js?v=sensevoice-13",
|
||||||
"/speech-audio-worklet.js?v=sensevoice-5",
|
"/speech-audio-worklet.js?v=sensevoice-5",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import json
|
|||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import uuid
|
||||||
from collections.abc import Callable, Mapping
|
from collections.abc import Callable, Mapping
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
@@ -157,6 +158,7 @@ class NfcExtensionToyPublisher:
|
|||||||
self._thread: threading.Thread | None = None
|
self._thread: threading.Thread | None = None
|
||||||
self._revision = 0
|
self._revision = 0
|
||||||
self._status: dict[str, Any] = {
|
self._status: dict[str, Any] = {
|
||||||
|
"session_id": None,
|
||||||
"state": "stopped",
|
"state": "stopped",
|
||||||
"topic": EXTENSION_TOY_MQTT_TOPIC,
|
"topic": EXTENSION_TOY_MQTT_TOPIC,
|
||||||
"qos": 1,
|
"qos": 1,
|
||||||
@@ -168,6 +170,7 @@ class NfcExtensionToyPublisher:
|
|||||||
"active_extension_names": [],
|
"active_extension_names": [],
|
||||||
"active_target_scopes": [],
|
"active_target_scopes": [],
|
||||||
"detected_nfc_ids": [],
|
"detected_nfc_ids": [],
|
||||||
|
"detection_count": 0,
|
||||||
"message_count": 0,
|
"message_count": 0,
|
||||||
"burst_frame_attempt_count": 0,
|
"burst_frame_attempt_count": 0,
|
||||||
"burst_publish_attempt_count": 0,
|
"burst_publish_attempt_count": 0,
|
||||||
@@ -188,12 +191,14 @@ class NfcExtensionToyPublisher:
|
|||||||
return
|
return
|
||||||
self._stop.clear()
|
self._stop.clear()
|
||||||
self._set_status(
|
self._set_status(
|
||||||
|
session_id=uuid.uuid4().hex,
|
||||||
state="waiting_for_nfc",
|
state="waiting_for_nfc",
|
||||||
active_nfc_id=None,
|
active_nfc_id=None,
|
||||||
active_catalog_revision=None,
|
active_catalog_revision=None,
|
||||||
active_extension_names=[],
|
active_extension_names=[],
|
||||||
active_target_scopes=[],
|
active_target_scopes=[],
|
||||||
detected_nfc_ids=[],
|
detected_nfc_ids=[],
|
||||||
|
detection_count=0,
|
||||||
message_count=0,
|
message_count=0,
|
||||||
last_error=None,
|
last_error=None,
|
||||||
)
|
)
|
||||||
@@ -211,12 +216,14 @@ class NfcExtensionToyPublisher:
|
|||||||
thread.join(timeout=2.0)
|
thread.join(timeout=2.0)
|
||||||
self._thread = None
|
self._thread = None
|
||||||
self._set_status(
|
self._set_status(
|
||||||
|
session_id=None,
|
||||||
state="stopped",
|
state="stopped",
|
||||||
active_nfc_id=None,
|
active_nfc_id=None,
|
||||||
active_catalog_revision=None,
|
active_catalog_revision=None,
|
||||||
active_extension_names=[],
|
active_extension_names=[],
|
||||||
active_target_scopes=[],
|
active_target_scopes=[],
|
||||||
detected_nfc_ids=[],
|
detected_nfc_ids=[],
|
||||||
|
detection_count=0,
|
||||||
message_count=0,
|
message_count=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -339,6 +346,7 @@ class NfcExtensionToyPublisher:
|
|||||||
|
|
||||||
def _run(self) -> None:
|
def _run(self) -> None:
|
||||||
latched_uid = ""
|
latched_uid = ""
|
||||||
|
present_uid = ""
|
||||||
latched_catalog_revision = ""
|
latched_catalog_revision = ""
|
||||||
latched_message_signature: str | None = None
|
latched_message_signature: str | None = None
|
||||||
while not self._stop.is_set():
|
while not self._stop.is_set():
|
||||||
@@ -349,6 +357,16 @@ class NfcExtensionToyPublisher:
|
|||||||
self._stop.wait(self.poll_interval_s)
|
self._stop.wait(self.poll_interval_s)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if card_uid != present_uid:
|
||||||
|
present_uid = card_uid
|
||||||
|
if card_uid:
|
||||||
|
with self._lock:
|
||||||
|
detection_count = int(self._status["detection_count"]) + 1
|
||||||
|
self._set_status(
|
||||||
|
active_nfc_id=card_uid,
|
||||||
|
detection_count=detection_count,
|
||||||
|
)
|
||||||
|
|
||||||
if card_uid:
|
if card_uid:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
detected_nfc_ids = list(self._status["detected_nfc_ids"])
|
detected_nfc_ids = list(self._status["detected_nfc_ids"])
|
||||||
|
|||||||
@@ -2976,6 +2976,48 @@ class SelfExtensionService:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def execute_quick_peripheral_capability(
|
||||||
|
self,
|
||||||
|
capability_id: object,
|
||||||
|
*,
|
||||||
|
conversation_id: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Execute one durable NFC-authorized capability selected by the UI."""
|
||||||
|
|
||||||
|
normalized_id = str(capability_id or "").strip()
|
||||||
|
if not normalized_id or len(normalized_id) > 128:
|
||||||
|
raise ValueError("外设快捷控制缺少有效的能力 ID。")
|
||||||
|
capability = next(
|
||||||
|
(
|
||||||
|
dict(item)
|
||||||
|
for item in self.list_peripheral_capabilities()
|
||||||
|
if isinstance(item, Mapping)
|
||||||
|
and str(item.get("id") or "").strip() == normalized_id
|
||||||
|
and item.get("status") == "learned"
|
||||||
|
and item.get("enabled") is True
|
||||||
|
and bool(_normalized_card_uid(item.get("card_uid")))
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if capability is None:
|
||||||
|
raise ExtensionIntegrationError(
|
||||||
|
"外设快捷控制未通过当前持久 NFC 能力目录校验。"
|
||||||
|
)
|
||||||
|
device_name = str(
|
||||||
|
capability.get("device_name")
|
||||||
|
or capability.get("device_id")
|
||||||
|
or "外设"
|
||||||
|
).strip()
|
||||||
|
action_name = str(capability.get("action") or "动作").strip()
|
||||||
|
result = self.execute_peripheral_request(
|
||||||
|
f"{device_name} {action_name}",
|
||||||
|
capability,
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
execution_mode="hardware",
|
||||||
|
provider_requested="web_console_quick_control",
|
||||||
|
)
|
||||||
|
return {"success": True, **result}
|
||||||
|
|
||||||
def execute_peripheral_plan_request(
|
def execute_peripheral_plan_request(
|
||||||
self,
|
self,
|
||||||
utterance: str,
|
utterance: str,
|
||||||
|
|||||||
@@ -4554,6 +4554,21 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
)
|
)
|
||||||
self._json({"success": True, **sync_result})
|
self._json({"success": True, **sync_result})
|
||||||
return
|
return
|
||||||
|
if path == "/api/extension/peripherals/execute":
|
||||||
|
result = self.extension.execute_quick_peripheral_capability(
|
||||||
|
payload.get("capability_id"),
|
||||||
|
conversation_id=str(payload.get("conversation_id") or "").strip()
|
||||||
|
or None,
|
||||||
|
)
|
||||||
|
self.vehicle_display_sync.update_extension(
|
||||||
|
action={
|
||||||
|
"type": "peripheral_quick_control",
|
||||||
|
"command": result.get("utterance"),
|
||||||
|
},
|
||||||
|
result=result,
|
||||||
|
)
|
||||||
|
self._json(result)
|
||||||
|
return
|
||||||
if path == "/api/extension/assistant/plan":
|
if path == "/api/extension/assistant/plan":
|
||||||
self.vehicle_display_sync.update_extension(
|
self.vehicle_display_sync.update_extension(
|
||||||
catalog=payload.get("peripherals", payload.get("catalog", [])),
|
catalog=payload.get("peripherals", payload.get("catalog", [])),
|
||||||
|
|||||||
@@ -1024,11 +1024,11 @@ operations:
|
|||||||
) as response:
|
) as response:
|
||||||
speech_worklet = response.read().decode("utf-8")
|
speech_worklet = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
f"{self.base_url}/self-extension.js?v=nfc-session-58", timeout=3
|
f"{self.base_url}/self-extension.js?v=peripheral-cards-59", timeout=3
|
||||||
) as response:
|
) as response:
|
||||||
extension_script = response.read().decode("utf-8")
|
extension_script = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
f"{self.base_url}/self-extension.css?v=pet-settings-54-dialog-size", timeout=3
|
f"{self.base_url}/self-extension.css?v=peripheral-cards-55", timeout=3
|
||||||
) as response:
|
) as response:
|
||||||
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
|
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
@@ -1105,7 +1105,7 @@ operations:
|
|||||||
self.assertIn('id="v2BootScreen"', template)
|
self.assertIn('id="v2BootScreen"', template)
|
||||||
self.assertIn('正在加载车机控制与实时状态', template)
|
self.assertIn('正在加载车机控制与实时状态', template)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
f'src="{runtime_url("self-extension.js", "/self-extension.js?v=nfc-session-58")}"',
|
f'src="{runtime_url("self-extension.js", "/self-extension.js?v=peripheral-cards-59")}"',
|
||||||
html,
|
html,
|
||||||
)
|
)
|
||||||
self.assertIn('import "../self-extension.css";', vite_entry)
|
self.assertIn('import "../self-extension.css";', vite_entry)
|
||||||
|
|||||||
@@ -131,6 +131,9 @@ def test_each_detection_publishes_three_frames_per_device_and_then_stops() -> No
|
|||||||
for _at, publication in publications
|
for _at, publication in publications
|
||||||
) == {"toy-fan-001": 3, "toy-light-001": 3}
|
) == {"toy-fan-001": 3, "toy-light-001": 3}
|
||||||
status = service.snapshot()
|
status = service.snapshot()
|
||||||
|
session_id = status["session_id"]
|
||||||
|
assert isinstance(session_id, str) and session_id
|
||||||
|
assert status["detection_count"] == 1
|
||||||
assert status["state"] == "burst_complete"
|
assert status["state"] == "burst_complete"
|
||||||
assert status["burst_frame_attempt_count"] == 3
|
assert status["burst_frame_attempt_count"] == 3
|
||||||
assert status["burst_publish_attempt_count"] == 6
|
assert status["burst_publish_attempt_count"] == 6
|
||||||
@@ -147,6 +150,8 @@ def test_each_detection_publishes_three_frames_per_device_and_then_stops() -> No
|
|||||||
nfc["card"] = {"action": "present", "uid": "E113B203"}
|
nfc["card"] = {"action": "present", "uid": "E113B203"}
|
||||||
assert wait_until(lambda: len(publications) == 12)
|
assert wait_until(lambda: len(publications) == 12)
|
||||||
assert service.snapshot()["state"] == "burst_complete"
|
assert service.snapshot()["state"] == "burst_complete"
|
||||||
|
assert service.snapshot()["session_id"] == session_id
|
||||||
|
assert service.snapshot()["detection_count"] == 2
|
||||||
finally:
|
finally:
|
||||||
service.close()
|
service.close()
|
||||||
|
|
||||||
@@ -172,6 +177,8 @@ def test_detected_devices_remain_latched_until_publisher_stops() -> None:
|
|||||||
service.close()
|
service.close()
|
||||||
|
|
||||||
assert service.snapshot()["detected_nfc_ids"] == []
|
assert service.snapshot()["detected_nfc_ids"] == []
|
||||||
|
assert service.snapshot()["session_id"] is None
|
||||||
|
assert service.snapshot()["detection_count"] == 0
|
||||||
|
|
||||||
|
|
||||||
def test_no_matching_ability_publishes_nothing() -> None:
|
def test_no_matching_ability_publishes_nothing() -> None:
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ def test_performance_assets_have_a_new_cache_generation() -> None:
|
|||||||
for asset in (
|
for asset in (
|
||||||
"/app.js?v=dual-seat-147-video-media",
|
"/app.js?v=dual-seat-147-video-media",
|
||||||
"/console-v2.js?v=local-first-79-cinema-status",
|
"/console-v2.js?v=local-first-79-cinema-status",
|
||||||
"/self-extension.js?v=nfc-session-58",
|
"/self-extension.js?v=peripheral-cards-59",
|
||||||
"/energy-monitor.js?v=energy-monitor-5-performance",
|
"/energy-monitor.js?v=energy-monitor-5-performance",
|
||||||
):
|
):
|
||||||
assert asset in html
|
assert asset in html
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None:
|
|||||||
for source_asset, cached_asset in (
|
for source_asset, cached_asset in (
|
||||||
('import "../styles.css"', "/styles.css?v=dual-seat-24-dialog-size"),
|
('import "../styles.css"', "/styles.css?v=dual-seat-24-dialog-size"),
|
||||||
('import "../console-v2.css"', "/console-v2.css?v=delivery-state-69"),
|
('import "../console-v2.css"', "/console-v2.css?v=delivery-state-69"),
|
||||||
('import "../self-extension.css"', "/self-extension.css?v=pet-settings-54-dialog-size"),
|
('import "../self-extension.css"', "/self-extension.css?v=peripheral-cards-55"),
|
||||||
('import "../speech-input.css"', "/speech-input.css?v=sensevoice-7"),
|
('import "../speech-input.css"', "/speech-input.css?v=sensevoice-7"),
|
||||||
('import "../codepen-glass-theme.css"', "/codepen-glass-theme.css?v=creative-cloud-6-performance"),
|
('import "../codepen-glass-theme.css"', "/codepen-glass-theme.css?v=creative-cloud-6-performance"),
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -67,13 +67,23 @@ def test_unified_console_exposes_device_id_based_nfc_authorization() -> None:
|
|||||||
assert 'signalSource: "backend_catalog"' in script
|
assert 'signalSource: "backend_catalog"' in script
|
||||||
assert '等待 NFC 外设靠近' in script
|
assert '等待 NFC 外设靠近' in script
|
||||||
assert '检测到已绑定的 NFC 后,外设将在本次运行期间持续显示' in script
|
assert '检测到已绑定的 NFC 后,外设将在本次运行期间持续显示' in script
|
||||||
|
assert 'data-hide-peripheral-card' in script
|
||||||
|
assert 'data-toggle-peripheral-card' in script
|
||||||
|
assert 'data-run-peripheral-quick-action' in script
|
||||||
|
assert 'function hidePeripheralCard(deviceId, cardUid)' in script
|
||||||
|
assert 'function togglePeripheralCard(deviceId)' in script
|
||||||
|
assert 'api("peripherals/execute"' in script
|
||||||
|
assert 'const hiddenPeripheralDevices = new Map()' in script
|
||||||
|
assert 'detection_count' in script
|
||||||
|
|
||||||
assert '.v2-nfc-inline' in styles
|
assert '.v2-nfc-inline' in styles
|
||||||
assert '.v2-nfc-dialog' in styles
|
assert '.v2-nfc-dialog' in styles
|
||||||
assert '.v2-nfc-command-list' in styles
|
assert '.v2-nfc-command-list' in styles
|
||||||
assert '.v2-nfc-command-option' in styles
|
assert '.v2-nfc-command-option' in styles
|
||||||
assert '.v2-nfc-select-all' in styles
|
assert '.v2-nfc-select-all' in styles
|
||||||
assert 'self-extension.js?v=nfc-session-58' in html
|
assert '.v2-extension-device-hide' in styles
|
||||||
|
assert '.v2-extension-quick-controls' in styles
|
||||||
|
assert 'self-extension.js?v=peripheral-cards-59' in html
|
||||||
assert 'import "../self-extension.css";' in vite_entry
|
assert 'import "../self-extension.css";' in vite_entry
|
||||||
|
|
||||||
|
|
||||||
@@ -144,6 +154,8 @@ def test_server_exposes_nfc_snapshot_through_same_origin_extension_api() -> None
|
|||||||
assert 'SeatDemoHandler.pets.start()' in server
|
assert 'SeatDemoHandler.pets.start()' in server
|
||||||
assert 'SeatDemoHandler.pets.close()' in server
|
assert 'SeatDemoHandler.pets.close()' in server
|
||||||
assert 'SeatDemoHandler.nfc.close()' in server
|
assert 'SeatDemoHandler.nfc.close()' in server
|
||||||
|
assert 'path == "/api/extension/peripherals/execute"' in server
|
||||||
|
assert "execute_quick_peripheral_capability" in server
|
||||||
assert "frame-src 'self' http://*:3001" in server
|
assert "frame-src 'self' http://*:3001" in server
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1403,6 +1403,40 @@ class SelfExtensionTests(unittest.TestCase):
|
|||||||
self.assertEqual(operation["domain"], "self_extension")
|
self.assertEqual(operation["domain"], "self_extension")
|
||||||
self.assertTrue(operation["telemetry"]["gateway_acknowledged"])
|
self.assertTrue(operation["telemetry"]["gateway_acknowledged"])
|
||||||
|
|
||||||
|
def test_quick_peripheral_control_reauthorizes_durable_capability(self) -> None:
|
||||||
|
with TemporaryDirectory() as directory:
|
||||||
|
service = SelfExtensionService(
|
||||||
|
SimulationGateway(),
|
||||||
|
catalog_db_path=Path(directory, "infrared.sqlite3"),
|
||||||
|
)
|
||||||
|
service.sync_peripheral_catalog(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "quick-light-on",
|
||||||
|
"status": "learned",
|
||||||
|
"enabled": True,
|
||||||
|
"device_id": "quick-light",
|
||||||
|
"device_name": "快捷灯",
|
||||||
|
"object_names": ["快捷灯"],
|
||||||
|
"card_uid": "2CC8B103",
|
||||||
|
"action": "打开",
|
||||||
|
"action_names": ["打开", "开启"],
|
||||||
|
"slot": 8,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
result = service.execute_quick_peripheral_capability(
|
||||||
|
"quick-light-on",
|
||||||
|
conversation_id="conv-quick-light",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(result["success"])
|
||||||
|
self.assertEqual(result["execution_mode"], "hardware")
|
||||||
|
self.assertEqual(result["provider_requested"], "web_console_quick_control")
|
||||||
|
self.assertEqual(result["peripheral"]["slot"], 8)
|
||||||
|
service.close()
|
||||||
|
|
||||||
def test_main_peripheral_sequence_executes_validated_actions_and_delays(self) -> None:
|
def test_main_peripheral_sequence_executes_validated_actions_and_delays(self) -> None:
|
||||||
service = SelfExtensionService(SimulationGateway())
|
service = SelfExtensionService(SimulationGateway())
|
||||||
catalog = [
|
catalog = [
|
||||||
|
|||||||
@@ -116,3 +116,18 @@ def test_homepage_only_reveals_nfc_devices_detected_in_this_run() -> None:
|
|||||||
assert "const detectedNfcIds = new Set(" in unified
|
assert "const detectedNfcIds = new Set(" in unified
|
||||||
assert "detectedNfcIds.has(normalizeCardUid(device.cardUid))" in unified
|
assert "detectedNfcIds.has(normalizeCardUid(device.cardUid))" in unified
|
||||||
assert "检测到已绑定的 NFC 后,外设将在本次运行期间持续显示" in unified
|
assert "检测到已绑定的 NFC 后,外设将在本次运行期间持续显示" in unified
|
||||||
|
|
||||||
|
|
||||||
|
def test_homepage_peripheral_cards_are_single_expand_and_quick_control_surfaces() -> None:
|
||||||
|
source = SOURCE_PATH.read_text(encoding="utf-8")
|
||||||
|
unified = _function_source(
|
||||||
|
source,
|
||||||
|
" function buildUnifiedExtensionSync() {",
|
||||||
|
"\n function init()",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "expandedPeripheralDeviceId === device.id" in unified
|
||||||
|
assert 'data-hide-peripheral-card="${escapeHtml(device.id)}"' in unified
|
||||||
|
assert 'data-toggle-peripheral-card="${escapeHtml(device.id)}"' in unified
|
||||||
|
assert 'data-run-peripheral-quick-action="${escapeHtml(action.id)}"' in unified
|
||||||
|
assert "hiddenPeripheralDevices.has(device.id)" in unified
|
||||||
|
|||||||
Reference in New Issue
Block a user