feat: improve mode and peripheral orchestration

This commit is contained in:
Miayui
2026-09-21 00:13:47 +08:00
parent 89707a42e2
commit 448c8b31a0
39 changed files with 3068 additions and 277 deletions
+83 -19
View File
@@ -382,6 +382,8 @@
};
let peripheralCatalogRevision = null;
let peripheralCatalogSyncQueue = Promise.resolve();
let durablePeripheralCatalog = [];
let durablePeripheralCatalogHydrated = false;
let petCardSnapshot = {
revision: -1,
cards: [],
@@ -1973,11 +1975,40 @@
}).filter((item) => item.device_id && item.card_uid && validSlot(item.slot));
}
function applyDurablePeripheralCatalog(
catalog,
revision = null,
{ render = true } = {},
) {
const nextCatalog = (Array.isArray(catalog) ? catalog : [])
.filter((item) => item && typeof item === "object"
&& item.id && item.device_id && normalizeCardUid(item.card_uid)
&& validSlot(item.slot))
.map((item) => ({ ...item, card_uid: normalizeCardUid(item.card_uid) }));
const changed = JSON.stringify(durablePeripheralCatalog) !== JSON.stringify(nextCatalog)
|| !durablePeripheralCatalogHydrated;
durablePeripheralCatalog = nextCatalog;
durablePeripheralCatalogHydrated = true;
if (revision != null && String(revision).trim()) {
peripheralCatalogRevision = String(revision).trim();
}
if (changed) unifiedExtensionDirty = true;
if (render && changed) renderUnifiedExtensionView?.({ force: true });
return changed;
}
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;
applyDurablePeripheralCatalog(
catalog,
payload.catalog_revision,
{ render: false },
);
if (!catalog.length) {
renderAll();
return 0;
}
const restoredDeviceIds = new Set();
catalog.forEach((item) => {
if (!item || typeof item !== "object" || !item.id || !validSlot(item.slot)) return;
@@ -3806,12 +3837,13 @@
}
async function syncPeripheralCatalog(device = null, { throwOnError = false } = {}) {
const nextCatalog = peripheralCatalog();
const operation = peripheralCatalogSyncQueue
.catch(() => null)
.then(() => api("display-sync", {
method: "POST",
body: JSON.stringify({
peripherals: peripheralCatalog(),
peripherals: nextCatalog,
device,
catalog_revision: peripheralCatalogRevision,
}),
@@ -3819,7 +3851,10 @@
peripheralCatalogSyncQueue = operation.catch(() => null);
try {
const result = await operation;
peripheralCatalogRevision = result.catalog_revision || peripheralCatalogRevision;
applyDurablePeripheralCatalog(
nextCatalog,
result.catalog_revision,
);
return result;
} catch (error) {
console.warn("外设能力同步失败", error);
@@ -4169,6 +4204,18 @@
status: item.status,
enabled: item.enabled,
})),
durableCatalogHydrated: durablePeripheralCatalogHydrated,
durableCatalog: durablePeripheralCatalog.map((item) => ({
id: item.id,
deviceId: item.device_id,
deviceName: item.device_name,
deviceType: item.device_type,
typeLabel: item.type_label,
action: item.action,
cardUid: item.card_uid,
status: item.status,
enabled: item.enabled,
})),
lastAction: snapshot?.extension?.last_action ? {
command: snapshot.extension.last_action.command,
type: snapshot.extension.last_action.type,
@@ -4249,27 +4296,28 @@
const sharedCatalog = Array.isArray(snapshot?.extension?.catalog)
? snapshot.extension.catalog
: [];
const mergedCatalog = [...sharedCatalog, ...peripheralCatalog()]
const fallbackCatalog = [...sharedCatalog, ...peripheralCatalog()]
.filter((item, index, catalog) => {
const identity = item.id || `${item.device_id}:${item.action}`;
return catalog.findIndex((candidate) => (
candidate.id || `${candidate.device_id}:${candidate.action}`
) === identity) === index;
});
const persistentCatalog = (
durablePeripheralCatalogHydrated
? durablePeripheralCatalog
: fallbackCatalog
).filter((item) => (
normalizeCardUid(item.card_uid)
&& item.status === "learned"
));
const nfcReader = nfcSnapshot.reader || {};
const nfcCard = nfcSnapshot.card;
const sensedCardUid = normalizeCardUid(nfcCard?.uid);
const nfcDevice = linkedDeviceForCard(sensedCardUid);
const nfcPet = linkedPetForCard(sensedCardUid);
const activeCatalog = sensedCardUid
? mergedCatalog.filter((item) => (
normalizeCardUid(item.card_uid) === sensedCardUid
&& item.status === "learned"
&& item.enabled !== false
))
: [];
const grouped = new Map();
activeCatalog.forEach((item) => {
persistentCatalog.forEach((item) => {
const deviceId = item.device_id || "external-device";
if (!grouped.has(deviceId)) grouped.set(deviceId, {
id: deviceId,
@@ -4278,7 +4326,10 @@
cardUid: item.card_uid || null,
actions: [],
});
grouped.get(deviceId).actions.push(item.action || item.name || "已学习能力");
const actionName = item.action || item.name || "已学习能力";
grouped.get(deviceId).actions.push(
item.enabled === false ? `${actionName}(已停用)` : actionName,
);
});
const devices = [...grouped.values()];
const lastAction = snapshot?.extension?.last_action;
@@ -4294,8 +4345,10 @@
: petCardSnapshot.publisher?.state === "burst_failed"
? "宠物消息发送失败"
: "已感应宠物 NFC"
: devices.length
: nfcDevice
? "已感应 NFC 外设"
: devices.length
? `${devices.length} 个外设已绑定`
: sensedCardUid
? "当前 NFC 暂无已授权命令"
: nfcReader.online
@@ -4361,11 +4414,11 @@
<div>${device.actions.slice(0, 8).map((action) => `<span>${escapeHtml(action)}</span>`).join("")}</div>
</article>`).join("") : nfcPet ? "" : `
<div class="v2-extension-empty">
<i data-lucide="${sensedCardUid ? "badge-alert" : "scan-line"}"></i><div><strong>${sensedCardUid ? "当前 NFC 暂无已授权命令" : "当前未感应到外设"}</strong><span>${sensedCardUid ? "请检查设备 ID 绑定及命令复选框" : "将已绑定设备 ID 的 NFC 卡靠近读卡器后显示命令"}</span></div>
<i data-lucide="link-2-off"></i><div><strong>当前没有已绑定外设</strong><span>外设绑定 NFC 后将持续显示,解除绑定后移除</span></div>
</div>`}
</div>
<footer class="v2-extension-linkage" data-active="${String(Boolean(visibleLastAction))}">
<i data-lucide="workflow"></i><div><small>最近联动</small><strong>${escapeHtml(visibleLastAction?.command || visibleLastAction?.type || "等待 NFC 外设感应")}</strong></div>
<i data-lucide="workflow"></i><div><small>最近联动</small><strong>${escapeHtml(visibleLastAction?.command || visibleLastAction?.type || "暂无外设联动")}</strong></div>
<span>${escapeHtml(visibleLastAction ? (lastResult?.status || lastResult?.message || "STANDBY") : "STANDBY")}</span>
</footer>`;
unifiedExtensionFingerprint = renderFingerprint;
@@ -4377,6 +4430,17 @@
window.addEventListener("cognitive-agent:display-sync", (event) => {
snapshot = event.detail || null;
const extensionSnapshot = snapshot?.extension;
if (
Array.isArray(extensionSnapshot?.catalog)
&& extensionSnapshot?.result?.catalog_revision
) {
applyDurablePeripheralCatalog(
extensionSnapshot.catalog,
extensionSnapshot.result.catalog_revision,
{ render: false },
);
}
if (snapshot?.extension_health?.nfc) {
applyNfcSnapshot(snapshot.extension_health.nfc, { announce: false });
}
@@ -4402,8 +4466,8 @@
void hydratePeripheralCatalog()
.catch((error) => {
console.warn("恢复后端红外能力目录失败,将保留浏览器本地目录。", error);
})
.finally(() => syncPeripheralCatalog());
return syncPeripheralCatalog();
});
return true;
}