"use client"; import { useEffect, useRef } from "react"; import { toast } from "sonner"; import { getStoredAuth } from "@/core/auth"; const AUTH_STORAGE_KEY = "deerflow.auth"; // 防抖窗口:跨标签页登录/重写 auth 常常在极短时间内连发多个 storage 事件 // (例如先写 token、再写用户信息)。等它"落定"再判断,避免抖动期的中间态 // (短暂 null / 半写状态)误触发动作。 const SETTLE_DELAY_MS = 1200; // 提示 toast 的稳定 id —— 同一次身份变化只保留一条,避免连发事件刷屏。 const IDENTITY_TOAST_ID = "auth-identity-changed"; function readUserId(): string | null { return getStoredAuth()?.user_id ?? null; } /** * Mount once at the app root. Watches `localStorage["deerflow.auth"]` for * cross-tab changes. * * 背景:`deerflow.auth` 在同源所有标签页之间共享。某个标签页登录/重写它时, * 其它标签页仍持有旧用户的内存态(会话列表、thread id、以及多智能体会商这类 * 纯内存的页面状态),与新 token 不一致,可能在"新对话"等操作上 404。 * * 旧实现:只要检测到 user_id 变化就 `window.location.reload()`。这会把**正在 * 使用的标签页**(例如正在跑多智能体会商第二步的那个)整页刷新,内存态全丢、 * 步骤回到第一步 —— 用户复制链接到新标签页打开(新标签页可能以不同/默认用户 * 重新登录并改写共享 auth)就会触发,体验很差。 * * 现实现(方案 1,收敛粗暴刷新): * - **真·退出登录**(auth 变为 null)→ 仍然跳登录页并刷新(本就该重新登录)。 * - **出现不同的非空用户**(另一个标签页登录成别的身份)→ **不再自动整页刷新**, * 改为弹一条常驻提示,把"是否刷新同步"的决定权交给用户,避免冲掉正在进行的工作。 * - 防抖 + 落定后二次校验,过滤掉同用户重写 / 抖动中间态带来的误触发。 */ export function AuthSyncWatcher() { const lastUserIdRef = useRef(readUserId()); useEffect(() => { let settleTimer: ReturnType | null = null; // storage 事件落定后再判断当前的真实身份并决定动作。 function evaluateSettledIdentity() { const nextUserId = readUserId(); if (nextUserId === lastUserIdRef.current) { // 同用户重写,或抖动后又回到原身份 —— 无需任何动作。 return; } if (nextUserId === null) { // 另一个标签页退出登录(或清空了 auth)→ 本标签页也该回到登录页。 lastUserIdRef.current = null; window.location.replace("#/login"); window.location.reload(); return; } // 出现了不同的非空用户。共享 auth 已被改成别的身份,继续在本标签页发请求 // 可能 404 / 鉴权失败,但**不**强制刷新 —— 用户可能正在本标签页干活 // (如多智能体会商),由用户自己决定何时刷新同步。 lastUserIdRef.current = nextUserId; toast.warning("检测到账号在其它标签页发生变化", { id: IDENTITY_TOAST_ID, description: "当前标签页仍是原会话。如需与新账号同步,请刷新页面。", duration: Infinity, action: { label: "刷新同步", onClick: () => window.location.reload(), }, }); } function handleStorage(event: StorageEvent) { // 只关心 auth 键的变化;event.key === null 表示 localStorage 被整体清空。 if (event.key !== null && event.key !== AUTH_STORAGE_KEY) { return; } if (settleTimer) { clearTimeout(settleTimer); } settleTimer = setTimeout(evaluateSettledIdentity, SETTLE_DELAY_MS); } window.addEventListener("storage", handleStorage); return () => { window.removeEventListener("storage", handleStorage); if (settleTimer) { clearTimeout(settleTimer); } }; }, []); return null; }