feat(agents): 智能体归属/发布操作前增加「账号已切换」提醒

- 新增 useLiveAccount 钩子:跟踪「实时」登录账号(随 storage/focus 刷新),
  并与组件挂载时捕获的账号对比;token 存 localStorage 会被同浏览器其他标签页
  共享,另一标签页登录会静默替换本页 API 所用账号
- agent-card / agent-detail-panel / publish-square-picker-dialog / NewAgentPage
  接入该钩子,切换账号后对归属/发布类操作做提醒,避免误归属

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.8 committed 2026-06-22 11:30:15 +08:00
1 parent 2b6424b3ff
commit 16ba5deb0e
5 files changed
+221 -7

No files matched your search

@@ -0,0 +1,82 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { getAccountDisplayName, getStoredAuth } from "@/core/auth";
import { isEmbedActive } from "@/core/embed/embed-session";
const AUTH_STORAGE_KEY = "deerflow.auth";
export interface LiveAccount {
/** 实时登录用户 id(随 storage/focus 刷新)。null = 未登录。 */
userId: string | null;
/** 实时显示名(邮箱 @ 前缀,否则 user_id)。 */
displayName: string;
/** 组件首次挂载时捕获的用户 id。 */
mountUserId: string | null;
/** 组件首次挂载时捕获的显示名。 */
mountDisplayName: string;
/**
* 当前登录账号与挂载时不一致 —— 即本视图加载后登录被切换过(通常是在另一个
* 标签页登录了别的账号)。归属/发布类操作必须据此提醒,避免误归属。
*/
switched: boolean;
}
/**
* 跟踪「实时」登录账号,而不是挂载时缓存的值。
*
* 鉴权 token 存在 localStorage(非嵌入态下被同浏览器所有标签页共享),所以在
* 另一个标签页登录会**静默替换**本标签页 API 调用所用的 token。任何会决定归属的
* 界面(创建/发布智能体)都必须显示请求实际将以哪个账号执行,并在会话中途被切换
* 账号时给出提示 —— 否则会出现「用 A 账号创建却归到 B 账号名下」。
*/
export function useLiveAccount(): LiveAccount {
const mountRef = useRef<{ id: string | null; name: string } | null>(null);
if (mountRef.current === null) {
const auth = getStoredAuth();
mountRef.current = {
id: auth?.user_id ?? null,
name: getAccountDisplayName(auth),
};
}
const [live, setLive] = useState(() => mountRef.current!);
useEffect(() => {
function refresh() {
const auth = getStoredAuth();
const id = auth?.user_id ?? null;
const name = getAccountDisplayName(auth);
setLive((prev) =>
prev.id === id && prev.name === name ? prev : { id, name },
);
}
// 嵌入(iframe)态鉴权走按标签页的 sessionStorage、不跨标签,收不到 storage 事件
// —— 只挂在正常态;focus/visibilitychange 兜底刷新两种形态都有效。
function onStorage(e: StorageEvent) {
if (e.key === AUTH_STORAGE_KEY) {
refresh();
}
}
if (!isEmbedActive()) {
window.addEventListener("storage", onStorage);
}
window.addEventListener("focus", refresh);
document.addEventListener("visibilitychange", refresh);
refresh();
return () => {
window.removeEventListener("storage", onStorage);
window.removeEventListener("focus", refresh);
document.removeEventListener("visibilitychange", refresh);
};
}, []);
const mount = mountRef.current!;
return {
userId: live.id,
displayName: live.name,
mountUserId: mount.id,
mountDisplayName: mount.name,
switched: live.id !== null && mount.id !== null && live.id !== mount.id,
};
}