/** * 开发期数据维护入口(仅 dev 构建暴露) * * ## 为什么需要 * * 会话/工作区/菜单配置都按用户存在 localStorage 里(`fmode_*_`)。 * 这些键**不会因为代码改动而清空** —— 之前灌过的模拟数据会一直留在浏览器里, * 表现为"代码明明改了,界面上还是旧数据"(实测踩过两次)。 * * ## 用法(浏览器控制台) * * ```js * __fmodeData.clearDemo() // 只删模拟数据($demo_ 前缀),保留真实会话 * __fmodeData.clearSessions() // 清空会话索引(下次进入会重新从 Matrix 扫描) * __fmodeData.dump() // 打印当前各键的数量,排查用 * __fmodeData.previewAlert() // 预览全局异常提示(调样式用,2026-11-10) * ``` * * 🔴 只清**本地索引**,不碰 Matrix 服务端消息。 */ interface DataApi { clearDemo: () => void; clearSessions: () => void; dump: () => Record; /** 预览全局异常提示(开发期调样式用) */ previewAlert: () => void; } export function installDataApi(): void { if (typeof window === 'undefined') return; const keysOf = (prefix: string) => Object.keys(localStorage).filter((k) => k.startsWith(prefix)); const read = (k: string): T[] => { try { const v = JSON.parse(localStorage.getItem(k) ?? '[]') as T[]; return Array.isArray(v) ? v : []; } catch { return []; } }; const api: DataApi = { clearDemo() { let removed = 0; for (const k of keysOf('fmode_chat_sessions_')) { if (k.endsWith('_removed')) continue; const kept = read<{ id?: string }>(k).filter( (s) => !String(s.id ?? '').startsWith('$demo_'), ); localStorage.setItem(k, JSON.stringify(kept)); } for (const k of keysOf('fmode_workspaces_')) { const kept = read<{ id?: string }>(k).filter( (w) => !String(w.id ?? '').startsWith('ws_demo_'), ); removed += kept.length; localStorage.setItem(k, JSON.stringify(kept)); } location.reload(); }, clearSessions() { for (const k of keysOf('fmode_chat_sessions_')) localStorage.removeItem(k); location.reload(); }, dump() { const out: Record = {}; for (const k of keysOf('fmode_')) { if (k === 'fmode_me' || k === 'fmode_session_token') { out[k] = '<已登录凭据,略>'; continue; } const raw = localStorage.getItem(k) ?? ''; try { const v = JSON.parse(raw); out[k] = Array.isArray(v) ? `${v.length} 项` : v; } catch { out[k] = raw.slice(0, 40); } } console.table(out); return out; }, /* * 预览全局异常提示(2026-11-10)。 * * 调样式时需要立即看到效果,而真实超时要等 3 分钟。 * 只做前端展示,不改任何数据。 */ previewAlert() { const svc = (window as unknown as { ng?: { get?: (t: unknown) => unknown } }).ng?.get?.( // 惰性取 AlertService:避免 data-tools 与核心服务产生静态循环依赖 (globalThis as unknown as { __alertToken?: unknown }).__alertToken, ); if (svc) return; // 无 DI 访问通道时的兜底:直接派发自定义事件,由 App 根组件监听 window.dispatchEvent(new CustomEvent('fmode:preview-alert')); }, }; (window as unknown as { __fmodeData?: DataApi }).__fmodeData = api; }