feat: integrate learned peripherals into mode creation
Expose safe NFC-bound infrared actions to semantic scene composition, require flashing-light actions for romantic and Mid-Autumn modes, and preserve mixed CAN/IR mode flows. Fix NFC device reassignment and retry extension/toy MQTT publication when an active card's binding catalog changes.
This commit is contained in:
@@ -44,7 +44,7 @@
|
||||
<script type="module" src="/gesture-control.js?v=android-pad-9"></script>
|
||||
<script src="/pwa.js?v=android-pad-3" defer></script>
|
||||
<script src="/console-v2.js?v=local-first-77-performance" defer></script>
|
||||
<script src="/self-extension.js?v=pet-settings-53-performance" defer></script>
|
||||
<script src="/self-extension.js?v=nfc-mqtt-56" defer></script>
|
||||
<script src="/assistant-chat.js?v=codex-assistant-34" defer></script>
|
||||
<script src="/energy-monitor.js?v=energy-monitor-5-performance" defer></script>
|
||||
<script src="/firmware-workbench-entry.js?v=3" defer></script>
|
||||
|
||||
@@ -373,6 +373,15 @@
|
||||
card: null,
|
||||
legacyProfiles: [],
|
||||
};
|
||||
let extensionToySnapshot = {
|
||||
revision: -1,
|
||||
state: "stopped",
|
||||
topic: "extension/toy",
|
||||
burst_published_count: 0,
|
||||
last_error: null,
|
||||
};
|
||||
let peripheralCatalogRevision = null;
|
||||
let peripheralCatalogSyncQueue = Promise.resolve();
|
||||
let petCardSnapshot = {
|
||||
revision: -1,
|
||||
cards: [],
|
||||
@@ -1966,6 +1975,7 @@
|
||||
|
||||
async function hydratePeripheralCatalog() {
|
||||
const payload = await api("peripherals");
|
||||
peripheralCatalogRevision = payload.catalog_revision || peripheralCatalogRevision;
|
||||
const catalog = Array.isArray(payload.catalog) ? payload.catalog : [];
|
||||
if (!catalog.length) return 0;
|
||||
const restoredDeviceIds = new Set();
|
||||
@@ -3754,6 +3764,12 @@
|
||||
${deviceGroups.length ? `<div class="v2-nfc-existing"><span>继续关联红外设备 ID(可选)</span><div><select id="extensionNfcExistingDevice" data-nfc-device-select>${deviceGroups.map((group) => `<option value="${escapeHtml(group.deviceId)}">${escapeHtml(group.deviceId)} — ${escapeHtml(group.objectNames.join(" / ") || "无对象描述")} — ${group.commands.length} 条命令</option>`).join("")}</select><button class="v2-extension-primary" type="button" data-nfc-bind-existing>关联设备 ID</button></div><small>宠物消息与红外设备授权相互独立,不会覆盖宠物卡片内容。</small></div>` : ""}`;
|
||||
} else {
|
||||
const group = deviceGroups.find((item) => item.deviceId === linkedDevice.id);
|
||||
const alternativeDeviceGroups = deviceGroups.filter(
|
||||
(item) => item.deviceId !== linkedDevice.id
|
||||
&& !normalizeCardUid(
|
||||
state.devices.find((device) => device.id === item.deviceId)?.cardUid,
|
||||
),
|
||||
);
|
||||
const capabilities = configuredCapabilities(linkedDevice.id);
|
||||
const enabledCount = capabilities.filter((capability) => capability.enabled !== false).length;
|
||||
const allEnabled = capabilities.length > 0 && enabledCount === capabilities.length;
|
||||
@@ -3766,6 +3782,7 @@
|
||||
${renderNfcPetBinding(linkedPet)}
|
||||
<div class="v2-nfc-capability-heading"><div><strong>允许此 NFC 执行的命令</strong></div><span>${enabledCount}/${capabilities.length} 已选择</span></div>
|
||||
${capabilities.length ? `<label class="v2-nfc-select-all"><input type="checkbox" data-nfc-select-all ${allEnabled ? "checked" : ""} /><span><strong>全选</strong><small>统一允许或禁止当前设备 ID 下的全部命令</small></span></label><div class="v2-nfc-command-list">${capabilities.map((capability) => `<label class="v2-nfc-command-option"><input type="checkbox" data-nfc-capability-enabled="${escapeHtml(capability.id)}" ${capability.enabled !== false ? "checked" : ""} /><span><strong>${escapeHtml(capability.name)}</strong><small>IR ${String(capability.slot).padStart(3, "0")} · ${escapeHtml((capability.aliases || []).join("、"))}</small></span></label>`).join("")}</div>` : `<div class="v2-nfc-no-devices"><i data-lucide="triangle-alert"></i><div><strong>该设备 ID 暂无有效命令</strong><span>槽位可能已删除或改绑到其他设备 ID,请重新检查红外设置。</span></div></div>`}
|
||||
${alternativeDeviceGroups.length ? `<div class="v2-nfc-existing v2-nfc-rebind"><span>将当前 NFC 改绑到新设备 ID</span><div><select id="extensionNfcExistingDevice" data-nfc-device-select>${alternativeDeviceGroups.map((item) => `<option value="${escapeHtml(item.deviceId)}">${escapeHtml(item.deviceId)} — ${escapeHtml(item.objectNames.join(" / ") || "无对象描述")} — ${item.commands.length} 条命令</option>`).join("")}</select><button class="v2-extension-primary" type="button" data-nfc-bind-existing>改绑设备 ID</button></div><small>改绑成功后,旧设备 ID 的 NFC 授权会解除,新设备的全部有效命令会默认启用。</small></div>` : ""}
|
||||
<div class="v2-nfc-recognized-actions"><span>复选框状态会持久化为自然语言执行白名单。</span><button type="button" data-nfc-unbind>解绑 NFC</button></div>`;
|
||||
}
|
||||
nfcDialogFingerprint = renderFingerprint;
|
||||
@@ -3788,19 +3805,32 @@
|
||||
document.body.append(dialog);
|
||||
}
|
||||
|
||||
async function syncPeripheralCatalog(device = null) {
|
||||
try {
|
||||
await api("display-sync", {
|
||||
async function syncPeripheralCatalog(device = null, { throwOnError = false } = {}) {
|
||||
const operation = peripheralCatalogSyncQueue
|
||||
.catch(() => null)
|
||||
.then(() => api("display-sync", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ peripherals: peripheralCatalog(), device }),
|
||||
});
|
||||
body: JSON.stringify({
|
||||
peripherals: peripheralCatalog(),
|
||||
device,
|
||||
catalog_revision: peripheralCatalogRevision,
|
||||
}),
|
||||
}));
|
||||
peripheralCatalogSyncQueue = operation.catch(() => null);
|
||||
try {
|
||||
const result = await operation;
|
||||
peripheralCatalogRevision = result.catalog_revision || peripheralCatalogRevision;
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.warn("外设能力同步失败", error);
|
||||
if (throwOnError) throw error;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function bindCardToDevice(device, cardUid) {
|
||||
async function bindCardToDevice(device, cardUid) {
|
||||
if (!device || !cardUid) return;
|
||||
const previousBindings = state.devices.map((item) => [item.id, item.cardUid || null]);
|
||||
const normalizedUid = normalizeCardUid(cardUid);
|
||||
state.devices.forEach((item) => {
|
||||
if (item.id !== device.id && normalizeCardUid(item.cardUid) === normalizedUid) {
|
||||
@@ -3812,62 +3842,167 @@
|
||||
state.selectedDeviceByType[device.type] = device.id;
|
||||
saveState();
|
||||
renderAll();
|
||||
void syncPeripheralCatalog(device);
|
||||
try {
|
||||
await syncPeripheralCatalog(device, { throwOnError: true });
|
||||
} catch (error) {
|
||||
previousBindings.forEach(([id, cardUidValue]) => {
|
||||
const item = state.devices.find((candidate) => candidate.id === id);
|
||||
if (item) item.cardUid = cardUidValue;
|
||||
});
|
||||
saveState();
|
||||
renderAll();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function bindCurrentNfcToExisting() {
|
||||
async function bindCurrentNfcToExisting() {
|
||||
const deviceId = $("#extensionNfcExistingDevice")?.value;
|
||||
reconcileDevicesAndCapabilitiesFromSlots();
|
||||
const device = state.devices.find((item) => item.id === deviceId);
|
||||
const cardUid = nfcSnapshot.card?.uid;
|
||||
if (!device || !cardUid) return;
|
||||
configuredCapabilities(device.id).forEach((capability) => {
|
||||
if (!cardUid) {
|
||||
showToast("当前没有可用于关联的 NFC 卡片。", "error");
|
||||
return;
|
||||
}
|
||||
if (!device) {
|
||||
showToast("未找到所选设备 ID,请刷新红外槽位后重试。", "error");
|
||||
return;
|
||||
}
|
||||
const capabilities = configuredCapabilities(device.id);
|
||||
if (!capabilities.length) {
|
||||
showToast(`设备 ID“${device.id}”没有可授权的有效红外命令。`, "error");
|
||||
return;
|
||||
}
|
||||
const previousDevice = linkedDeviceForCard(cardUid);
|
||||
const previousEnabled = capabilities.map((capability) => [capability.id, capability.enabled]);
|
||||
capabilities.forEach((capability) => {
|
||||
capability.enabled = true;
|
||||
});
|
||||
bindCardToDevice(device, cardUid);
|
||||
try {
|
||||
await bindCardToDevice(device, cardUid);
|
||||
} catch (error) {
|
||||
previousEnabled.forEach(([id, enabled]) => {
|
||||
const capability = capabilities.find((item) => item.id === id);
|
||||
if (capability) capability.enabled = enabled;
|
||||
});
|
||||
saveState();
|
||||
renderAll();
|
||||
renderNfcDialog();
|
||||
showToast(`NFC 设备授权保存失败:${error.message}`, "error");
|
||||
return;
|
||||
}
|
||||
renderNfcDialog();
|
||||
showToast(`NFC 已关联设备 ID“${device.id}”,全部命令已默认选中。`, "success");
|
||||
showToast(
|
||||
previousDevice && previousDevice.id !== device.id
|
||||
? `NFC 已从设备 ID“${previousDevice.id}”改绑到“${device.id}”,新设备命令已全部启用。`
|
||||
: `NFC 已关联设备 ID“${device.id}”,全部命令已默认选中。`,
|
||||
"success",
|
||||
);
|
||||
}
|
||||
|
||||
function unbindCurrentNfc() {
|
||||
async function unbindCurrentNfc() {
|
||||
const device = linkedDeviceForCard(nfcSnapshot.card?.uid);
|
||||
if (!device) return;
|
||||
const previousUid = device.cardUid;
|
||||
device.cardUid = null;
|
||||
saveState();
|
||||
renderAll();
|
||||
void syncPeripheralCatalog(device);
|
||||
try {
|
||||
await syncPeripheralCatalog(device, { throwOnError: true });
|
||||
} catch (error) {
|
||||
device.cardUid = previousUid;
|
||||
saveState();
|
||||
renderAll();
|
||||
renderNfcDialog();
|
||||
showToast(`解除 NFC 绑定失败:${error.message}`, "error");
|
||||
return;
|
||||
}
|
||||
renderNfcDialog();
|
||||
showToast(`已解除设备 ID“${device.id}”的 NFC 绑定。`);
|
||||
}
|
||||
|
||||
function updateNfcCapabilityAuthorization(capabilityId, enabled) {
|
||||
async function updateNfcCapabilityAuthorization(capabilityId, enabled) {
|
||||
const device = linkedDeviceForCard(nfcSnapshot.card?.uid);
|
||||
const capability = configuredCapabilities(device?.id).find(
|
||||
(item) => item.id === capabilityId,
|
||||
);
|
||||
if (!device || !capability) return;
|
||||
const previousEnabled = capability.enabled;
|
||||
capability.enabled = enabled;
|
||||
saveState();
|
||||
renderAll();
|
||||
renderNfcDialog();
|
||||
void syncPeripheralCatalog(device);
|
||||
try {
|
||||
await syncPeripheralCatalog(device, { throwOnError: true });
|
||||
} catch (error) {
|
||||
capability.enabled = previousEnabled;
|
||||
saveState();
|
||||
renderAll();
|
||||
renderNfcDialog();
|
||||
showToast(`NFC 命令授权保存失败:${error.message}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function updateAllNfcCapabilityAuthorizations(enabled) {
|
||||
async function updateAllNfcCapabilityAuthorizations(enabled) {
|
||||
const device = linkedDeviceForCard(nfcSnapshot.card?.uid);
|
||||
if (!device) return;
|
||||
configuredCapabilities(device.id).forEach((capability) => {
|
||||
const capabilities = configuredCapabilities(device.id);
|
||||
const previousEnabled = capabilities.map((capability) => [capability.id, capability.enabled]);
|
||||
capabilities.forEach((capability) => {
|
||||
capability.enabled = enabled;
|
||||
});
|
||||
saveState();
|
||||
renderAll();
|
||||
renderNfcDialog();
|
||||
void syncPeripheralCatalog(device);
|
||||
try {
|
||||
await syncPeripheralCatalog(device, { throwOnError: true });
|
||||
} catch (error) {
|
||||
previousEnabled.forEach(([id, previous]) => {
|
||||
const capability = capabilities.find((item) => item.id === id);
|
||||
if (capability) capability.enabled = previous;
|
||||
});
|
||||
saveState();
|
||||
renderAll();
|
||||
renderNfcDialog();
|
||||
showToast(`NFC 命令授权保存失败:${error.message}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function applyExtensionToySnapshot(value, { announce = true } = {}) {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const revision = Number(value.revision ?? -1);
|
||||
if (revision === extensionToySnapshot.revision) return false;
|
||||
extensionToySnapshot = {
|
||||
revision,
|
||||
state: String(value.state || "stopped"),
|
||||
topic: String(value.topic || "extension/toy"),
|
||||
burst_published_count: Number(value.burst_published_count || 0),
|
||||
active_extension_names: Array.isArray(value.active_extension_names)
|
||||
? value.active_extension_names.map((item) => String(item || "")).filter(Boolean)
|
||||
: [],
|
||||
last_delivery_status: value.last_delivery_status || null,
|
||||
last_error: value.last_error || null,
|
||||
};
|
||||
if (announce && extensionToySnapshot.state === "burst_complete") {
|
||||
showToast(
|
||||
`已向 ${extensionToySnapshot.topic} 发送 ${extensionToySnapshot.burst_published_count} 帧设备能力消息。`,
|
||||
"success",
|
||||
);
|
||||
} else if (announce && extensionToySnapshot.state === "burst_failed") {
|
||||
showToast(
|
||||
`${extensionToySnapshot.topic} 发送失败:${extensionToySnapshot.last_error || "MQTT 未连接"}`,
|
||||
"error",
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyNfcSnapshot(value, { announce = true } = {}) {
|
||||
if (!value || typeof value !== "object") return;
|
||||
if (value.pet_cards) applyPetCardSnapshot(value.pet_cards);
|
||||
if (value.extension_toy) {
|
||||
applyExtensionToySnapshot(value.extension_toy, { announce });
|
||||
}
|
||||
const revision = Number(value.revision ?? -1);
|
||||
if (revision === nfcSnapshot.revision) return;
|
||||
if (Array.isArray(value.legacy_profiles) && value.legacy_profiles.length) {
|
||||
@@ -3893,8 +4028,8 @@
|
||||
if (announce) {
|
||||
showToast(
|
||||
pet
|
||||
? `已识别宠物“${pet.pet_name}”和设备 ID“${device.id}”。`
|
||||
: `已识别设备 ID“${device.id}”。`,
|
||||
? `已识别宠物“${pet.pet_name}”和设备 ID“${device.id}”,正在准备 extension/toy 消息。`
|
||||
: `已识别设备 ID“${device.id}”,正在准备 extension/toy 消息。`,
|
||||
"success",
|
||||
);
|
||||
}
|
||||
@@ -3958,8 +4093,8 @@
|
||||
return;
|
||||
}
|
||||
if (event.target.closest("[data-close-nfc-dialog]")) closeDialog("extensionNfcDialog");
|
||||
if (event.target.closest("[data-nfc-bind-existing]")) bindCurrentNfcToExisting();
|
||||
if (event.target.closest("[data-nfc-unbind]")) unbindCurrentNfc();
|
||||
if (event.target.closest("[data-nfc-bind-existing]")) void bindCurrentNfcToExisting();
|
||||
if (event.target.closest("[data-nfc-unbind]")) void unbindCurrentNfc();
|
||||
if (event.target.closest("[data-nfc-bind-pet]")) void bindCurrentNfcToPet();
|
||||
const unbindPet = event.target.closest("[data-nfc-unbind-pet]");
|
||||
if (unbindPet) void unbindPetCard(unbindPet.dataset.nfcUnbindPet);
|
||||
@@ -3969,14 +4104,14 @@
|
||||
if (petSelect) selectedPetCardId = petSelect.value;
|
||||
const capability = event.target.closest("[data-nfc-capability-enabled]");
|
||||
if (capability) {
|
||||
updateNfcCapabilityAuthorization(
|
||||
void updateNfcCapabilityAuthorization(
|
||||
capability.dataset.nfcCapabilityEnabled,
|
||||
capability.checked,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const selectAll = event.target.closest("[data-nfc-select-all]");
|
||||
if (selectAll) updateAllNfcCapabilityAuthorizations(selectAll.checked);
|
||||
if (selectAll) void updateAllNfcCapabilityAuthorizations(selectAll.checked);
|
||||
});
|
||||
$("#extensionContinueLearn").addEventListener("click", () => {
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const CACHE_VERSION = "cognitive-agent-pad-v325-notification-ui";
|
||||
const CACHE_VERSION = "cognitive-agent-pad-v328-nfc-mqtt";
|
||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
||||
const APP_SHELL = [
|
||||
@@ -12,7 +12,7 @@ const APP_SHELL = [
|
||||
"/console-v2.css?v=delivery-state-69",
|
||||
"/console-v2.js?v=local-first-77-performance",
|
||||
"/self-extension.css?v=pet-settings-54-dialog-size",
|
||||
"/self-extension.js?v=pet-settings-53-performance",
|
||||
"/self-extension.js?v=nfc-mqtt-56",
|
||||
"/speech-input.css?v=sensevoice-7",
|
||||
"/speech-input.js?v=sensevoice-13",
|
||||
"/speech-audio-worklet.js?v=sensevoice-5",
|
||||
|
||||
Reference in New Issue
Block a user