第二步消息列表长文件名不再溢出卡片;第三步绘制结束后从沙箱回灌完整 HTML(修复 append 续写 stub 被截短),进入第三步时关闭第二步沙箱;HTML 预览与提示词强调 utf-8/中文。跨标签页切换账号改为 toast 提示而非强制刷新;方案总结智能体 SOUL 去掉「AI 自动生成」标签要求。
100 lines
4.0 KiB
TypeScript
100 lines
4.0 KiB
TypeScript
"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<string | null>(readUserId());
|
|
|
|
useEffect(() => {
|
|
let settleTimer: ReturnType<typeof setTimeout> | 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;
|
|
}
|