data-tools.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. /**
  2. * 开发期数据维护入口(仅 dev 构建暴露)
  3. *
  4. * ## 为什么需要
  5. *
  6. * 会话/工作区/菜单配置都按用户存在 localStorage 里(`fmode_*_<uid>`)。
  7. * 这些键**不会因为代码改动而清空** —— 之前灌过的模拟数据会一直留在浏览器里,
  8. * 表现为"代码明明改了,界面上还是旧数据"(实测踩过两次)。
  9. *
  10. * ## 用法(浏览器控制台)
  11. *
  12. * ```js
  13. * __fmodeData.clearDemo() // 只删模拟数据($demo_ 前缀),保留真实会话
  14. * __fmodeData.clearSessions() // 清空会话索引(下次进入会重新从 Matrix 扫描)
  15. * __fmodeData.dump() // 打印当前各键的数量,排查用
  16. * __fmodeData.previewAlert() // 预览全局异常提示(调样式用,2026-11-10)
  17. * ```
  18. *
  19. * 🔴 只清**本地索引**,不碰 Matrix 服务端消息。
  20. */
  21. interface DataApi {
  22. clearDemo: () => void;
  23. clearSessions: () => void;
  24. dump: () => Record<string, unknown>;
  25. /** 预览全局异常提示(开发期调样式用) */
  26. previewAlert: () => void;
  27. }
  28. export function installDataApi(): void {
  29. if (typeof window === 'undefined') return;
  30. const keysOf = (prefix: string) =>
  31. Object.keys(localStorage).filter((k) => k.startsWith(prefix));
  32. const read = <T>(k: string): T[] => {
  33. try {
  34. const v = JSON.parse(localStorage.getItem(k) ?? '[]') as T[];
  35. return Array.isArray(v) ? v : [];
  36. } catch {
  37. return [];
  38. }
  39. };
  40. const api: DataApi = {
  41. clearDemo() {
  42. let removed = 0;
  43. for (const k of keysOf('fmode_chat_sessions_')) {
  44. if (k.endsWith('_removed')) continue;
  45. const kept = read<{ id?: string }>(k).filter(
  46. (s) => !String(s.id ?? '').startsWith('$demo_'),
  47. );
  48. localStorage.setItem(k, JSON.stringify(kept));
  49. }
  50. for (const k of keysOf('fmode_workspaces_')) {
  51. const kept = read<{ id?: string }>(k).filter(
  52. (w) => !String(w.id ?? '').startsWith('ws_demo_'),
  53. );
  54. removed += kept.length;
  55. localStorage.setItem(k, JSON.stringify(kept));
  56. }
  57. location.reload();
  58. },
  59. clearSessions() {
  60. for (const k of keysOf('fmode_chat_sessions_')) localStorage.removeItem(k);
  61. location.reload();
  62. },
  63. dump() {
  64. const out: Record<string, unknown> = {};
  65. for (const k of keysOf('fmode_')) {
  66. if (k === 'fmode_me' || k === 'fmode_session_token') {
  67. out[k] = '<已登录凭据,略>';
  68. continue;
  69. }
  70. const raw = localStorage.getItem(k) ?? '';
  71. try {
  72. const v = JSON.parse(raw);
  73. out[k] = Array.isArray(v) ? `${v.length} 项` : v;
  74. } catch {
  75. out[k] = raw.slice(0, 40);
  76. }
  77. }
  78. console.table(out);
  79. return out;
  80. },
  81. /*
  82. * 预览全局异常提示(2026-11-10)。
  83. *
  84. * 调样式时需要立即看到效果,而真实超时要等 3 分钟。
  85. * 只做前端展示,不改任何数据。
  86. */
  87. previewAlert() {
  88. const svc = (window as unknown as { ng?: { get?: (t: unknown) => unknown } }).ng?.get?.(
  89. // 惰性取 AlertService:避免 data-tools 与核心服务产生静态循环依赖
  90. (globalThis as unknown as { __alertToken?: unknown }).__alertToken,
  91. );
  92. if (svc) return;
  93. // 无 DI 访问通道时的兜底:直接派发自定义事件,由 App 根组件监听
  94. window.dispatchEvent(new CustomEvent('fmode:preview-alert'));
  95. },
  96. };
  97. (window as unknown as { __fmodeData?: DataApi }).__fmodeData = api;
  98. }