const OPENCLAW_3D_VIEWER_SESSION_PREFIX = "openclaw-3d-viewer:v1:"; const OPENCLAW_3D_VIEWER_SESSION_MAX_AGE_MS = 24 * 60 * 60 * 1000; export type OpenClaw3DViewerSession = { messageId: string; viewMode: "product" | "packaging"; explodeAmount: number; packagingExplodeAmount: number; selectedNodeName: string | null; selectedPackagingNodeName: string | null; updatedAt: number; }; type ViewerSessionStorage = Pick; function getViewerSessionKey(chatId: string) { return `${OPENCLAW_3D_VIEWER_SESSION_PREFIX}${chatId}`; } function removeViewerSession(storage: ViewerSessionStorage, key: string) { try { storage.removeItem(key); } catch { // Storage can be unavailable under strict browser privacy policies. } } function normalizeExplodeAmount(value: unknown) { const amount = typeof value === "number" ? value : Number(value); if (!Number.isFinite(amount)) return 0; return Math.max(0, Math.min(1, amount)); } export function readOpenClaw3DViewerSession(params: { storage: ViewerSessionStorage; chatId: string; now?: number; }): OpenClaw3DViewerSession | null { const key = getViewerSessionKey(params.chatId); try { const raw = params.storage.getItem(key); if (!raw) return null; const value = JSON.parse(raw) as Record; const messageId = typeof value.messageId === "string" ? value.messageId.trim() : ""; const updatedAt = Number(value.updatedAt); const now = params.now ?? Date.now(); if ( !messageId || !Number.isFinite(updatedAt) || updatedAt <= 0 || now - updatedAt > OPENCLAW_3D_VIEWER_SESSION_MAX_AGE_MS ) { removeViewerSession(params.storage, key); return null; } return { messageId, viewMode: value.viewMode === "packaging" ? "packaging" : "product", explodeAmount: normalizeExplodeAmount( value.productExplodeAmount ?? value.explodeAmount ), packagingExplodeAmount: value.packagingExplodeAmount === undefined ? 1 : normalizeExplodeAmount(value.packagingExplodeAmount), selectedNodeName: typeof value.selectedNodeName === "string" && value.selectedNodeName.trim() ? value.selectedNodeName.trim() : null, selectedPackagingNodeName: typeof value.selectedPackagingNodeName === "string" && value.selectedPackagingNodeName.trim() ? value.selectedPackagingNodeName.trim() : null, updatedAt, }; } catch { removeViewerSession(params.storage, key); return null; } } export function writeOpenClaw3DViewerSession(params: { storage: ViewerSessionStorage; chatId: string; messageId: string; explodeAmount: number; viewMode?: "product" | "packaging"; packagingExplodeAmount?: number; selectedNodeName?: string | null; selectedPackagingNodeName?: string | null; now?: number; }) { const messageId = params.messageId.trim(); if (!messageId) return; try { params.storage.setItem( getViewerSessionKey(params.chatId), JSON.stringify({ messageId, viewMode: params.viewMode || "product", explodeAmount: normalizeExplodeAmount(params.explodeAmount), packagingExplodeAmount: params.packagingExplodeAmount === undefined ? 1 : normalizeExplodeAmount(params.packagingExplodeAmount), selectedNodeName: params.selectedNodeName?.trim() || null, selectedPackagingNodeName: params.selectedPackagingNodeName?.trim() || null, updatedAt: params.now ?? Date.now(), } satisfies OpenClaw3DViewerSession) ); } catch { // Private browsing and storage policies may disable sessionStorage. } } export function clearOpenClaw3DViewerSession(params: { storage: ViewerSessionStorage; chatId: string; messageId: string; }) { const current = readOpenClaw3DViewerSession({ storage: params.storage, chatId: params.chatId, }); if (current?.messageId !== params.messageId) return; removeViewerSession(params.storage, getViewerSessionKey(params.chatId)); }