'use client'; // ==================== 持久化适配器 ==================== // 统一的存储接口:优先使用服务端文件系统,fallback 到浏览器 IndexedDB/localStorage // 多用户隔离:首次访问自动生成 userId 并存入 cookie import { get, set, del } from 'idb-keyval'; // ==================== 用户ID管理 ==================== function getUserId() { if (typeof document === 'undefined') return null; const match = document.cookie.match(/author-uid=([a-zA-Z0-9_-]+)/); return match ? match[1] : null; } function ensureUserId() { let uid = getUserId(); if (!uid) { uid = 'u-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8); // 设置 365 天有效的 cookie(HttpOnly = false,前端可读) document.cookie = `author-uid=${uid}; path=/; max-age=${365 * 24 * 3600}; SameSite=Lax`; } return uid; } // ==================== 服务端存储 ==================== let _serverAvailable = null; // null = 未检测, true/false = 检测结果 async function checkServerAvailable() { if (_serverAvailable !== null) return _serverAvailable; try { const res = await fetch('/api/storage?key=__ping', { method: 'GET', credentials: 'include', }); _serverAvailable = res.ok || res.status === 401; // 401 也说明服务端在运行 return _serverAvailable; } catch { _serverAvailable = false; return false; } } async function serverGet(key) { const res = await fetch(`/api/storage?key=${encodeURIComponent(key)}`, { method: 'GET', credentials: 'include', }); if (!res.ok) throw new Error(`Server GET failed: ${res.status}`); const { data } = await res.json(); return data; } async function serverSet(key, value) { const res = await fetch('/api/storage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ key, value }), }); if (!res.ok) throw new Error(`Server POST failed: ${res.status}`); } async function serverDel(key) { const res = await fetch(`/api/storage?key=${encodeURIComponent(key)}`, { method: 'DELETE', credentials: 'include', }); if (!res.ok) throw new Error(`Server DELETE failed: ${res.status}`); } // ==================== 统一存储接口 ==================== /** * 读取数据(服务端优先,fallback 到 IndexedDB) * @param {string} key - 存储键名 * @returns {Promise} 存储的值,不存在时返回 undefined */ export async function persistGet(key) { if (typeof window === 'undefined') return undefined; ensureUserId(); try { if (await checkServerAvailable()) { const data = await serverGet(key); if (data !== null && data !== undefined) return data; // 服务端没有,尝试从浏览器迁移 const localData = await browserGet(key); if (localData !== null && localData !== undefined) { // 自动迁移到服务端 await serverSet(key, localData).catch(() => { }); return localData; } return undefined; } } catch { // 服务端失败,降级到浏览器 } return await browserGet(key); } /** * 写入数据(同时写入服务端和浏览器) * @param {string} key - 存储键名 * @param {any} value - 要存储的值 */ export async function persistSet(key, value) { if (typeof window === 'undefined') return; ensureUserId(); // 先写浏览器(立即可用) await browserSet(key, value); // 异步写服务端(不阻塞 UI) if (await checkServerAvailable()) { serverSet(key, value).catch(err => { console.warn('[persist] Server write failed, data saved in browser only:', err.message); }); } } /** * 删除数据 * @param {string} key - 存储键名 */ export async function persistDel(key) { if (typeof window === 'undefined') return; await browserDel(key); if (await checkServerAvailable()) { serverDel(key).catch(() => { }); } } // ==================== 浏览器存储桥接 ==================== // 大数据用 IndexedDB,小数据用 localStorage const LOCALSTORAGE_KEYS = new Set([ 'author-project-settings', 'author-active-work', 'author-token-stats', 'author-theme', 'author-lang', 'author-visual', 'author-onboarding-done', 'author-context-selection', 'author-api-profiles', 'author-delete-never-remind', 'author-delete-skip-today', ]); async function browserGet(key) { if (LOCALSTORAGE_KEYS.has(key)) { const raw = localStorage.getItem(key); if (raw === null) return undefined; try { return JSON.parse(raw); } catch { return raw; } } const val = await get(key); return val === undefined ? undefined : val; } async function browserSet(key, value) { if (LOCALSTORAGE_KEYS.has(key)) { localStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value)); return; } await set(key, value); } async function browserDel(key) { if (LOCALSTORAGE_KEYS.has(key)) { localStorage.removeItem(key); return; } await del(key); } // ==================== 便捷方法 ==================== /** * 同步读取 localStorage(仅用于需要同步值的场景,如初始化 zustand store) * 不走服务端。 */ export function persistGetSync(key) { if (typeof window === 'undefined') return undefined; const raw = localStorage.getItem(key); if (raw === null) return undefined; try { return JSON.parse(raw); } catch { return raw; } } /** * 初始化:确保 userId 存在,并触发服务端可用性检测 * 应在应用启动时调用一次 */ export async function initPersistence() { if (typeof window === 'undefined') return; ensureUserId(); await checkServerAvailable(); }