import { useEffect, type Dispatch, type MutableRefObject, type SetStateAction } from 'react'; import { authClient } from '../api/auth'; import { fetchChatSessions } from '../api/chatSessions'; import { fetchProjects } from '../api/projectWorkflow'; import { loginMemberSyncIds, RequestEpoch, seedViewerMembership } from '../data/initFanout'; import { mergeSessionUser } from '../lib/mergeSessionUser'; import { mapServerProjectToProjectInfo, mergeProjectLists } from '../lib/projectAdapter'; import { mapServerSessionToProjectSession, mergeSessionLists } from '../lib/sessionAdapter'; import type { LoginResult, SystemAuthConfig } from '../types/auth'; import type { UserProfile } from '../types/docgov'; import type { ProjectInfo, ProjectSession } from '../types/workbench'; export interface AuthenticatedWorkspaceSyncInput { allUsers: UserProfile[]; setAllUsers: Dispatch>; setCurrentUser: Dispatch>; setSession: Dispatch>; setSessionValidated: Dispatch>; setSystemEnv: Dispatch>; setSystemAuthConfig: Dispatch>; setCapabilities: Dispatch>; session: LoginResult | null; sessionValidated: boolean | null; currentUser: UserProfile; currentProjectId: number; syncProjectMembersFromServer: (projectId: number, creatorId: number) => Promise; setProjects: Dispatch>; setProjectSyncNotice: Dispatch>; projectMembersModalOpen: boolean; projectsRef: MutableRefObject; setSessions: Dispatch>; setSessionSyncNotice: Dispatch>; sessionEpochRef: MutableRefObject; } /** * 已登录工作台的启动同步:校验会话、拉项目、按需同步当前项目成员、拉会话列表。 * 首次校验故意只跑一次,沿用入口组件原来的空依赖,避免用户目录更新把登录态重放一遍。 */ export function useAuthenticatedWorkspaceSync(input: AuthenticatedWorkspaceSyncInput): void { const { allUsers, setAllUsers, setCurrentUser, setSession, setSessionValidated, setSystemEnv, setSystemAuthConfig, setCapabilities, session, sessionValidated, currentUser, currentProjectId, syncProjectMembersFromServer, setProjects, setProjectSyncNotice, projectMembersModalOpen, projectsRef, setSessions, setSessionSyncNotice, sessionEpochRef, } = input; useEffect(() => { authClient.getSystemAuthConfig().then(setSystemAuthConfig).catch(() => {}); const saved = authClient.loadSavedSession(); if (saved) { setSession(saved); if (saved.env) setSystemEnv(saved.env); setCapabilities(saved.capabilities ?? []); const match = allUsers.find((u) => u.id === saved.user.id); setCurrentUser(mergeSessionUser(saved.user, match)); void authClient .fetchMe() .then((me) => { setCapabilities(me.capabilities ?? []); setCurrentUser(mergeSessionUser(me.user, match)); // Rewrite session from /auth/me so a polluted local mustChangePassword // (nested user flag) cannot survive refresh after a successful change. const nextSession = { ...saved, user: { ...saved.user, ...me.user, mustChangePassword: me.mustChangePassword }, mustChangePassword: me.mustChangePassword, env: me.env || saved.env, capabilities: me.capabilities ?? saved.capabilities ?? [], }; setSession(nextSession); authClient.saveSession(nextSession); setSessionValidated(true); }) .catch(() => { authClient.clearLocalSession(); setSession(null); setCapabilities([]); setSessionValidated(false); }); } else { setSessionValidated(false); } // 与原入口一致:只在挂载时恢复本地会话。 // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 会话校验通过后拉取全员轻量目录(普通用户可用,修复负责人选择器「用户 N」兜底)。 // 独立 effect + 不依赖 allUsers,避免与项目同步 effect 循环触发。 useEffect(() => { if (!session || sessionValidated !== true) return; let cancelled = false; void (async () => { try { const directory = await authClient.fetchUserDirectory(); if (cancelled) return; setAllUsers( directory.map((u) => ({ id: u.id, name: u.name, employeeId: u.employeeId || '', department: u.department || '', position: u.position || '', roleId: u.roleId, avatar: u.avatar, })), ); } catch (err) { if (cancelled) return; console.warn('[users] 用户目录同步失败,继续使用本地缓存:', err); } })(); return () => { cancelled = true; }; }, [session, sessionValidated, setAllUsers]); useEffect(() => { if (!session || sessionValidated !== true) { setProjectSyncNotice(null); return; } let cancelled = false; void (async () => { try { const serverProjects = await fetchProjects(); if (cancelled) return; setProjects((prev) => { const mapped = (serverProjects ?? []).map((project) => seedViewerMembership( mapServerProjectToProjectInfo( project, allUsers, prev.find((item) => item.id === project.id) ), currentUser.id, currentUser.name ) ); return mergeProjectLists(prev, mapped); }); const syncIds = loginMemberSyncIds( (serverProjects ?? []).map((project) => project.id), currentProjectId ); await Promise.all( syncIds.map((id) => { const project = serverProjects.find((item) => item.id === id); if (!project) return Promise.resolve(null); return syncProjectMembersFromServer(project.id, project.creator_user_id); }) ); setProjectSyncNotice(null); } catch (err) { if (cancelled) return; console.warn('[projects] 服务端项目列表同步失败,继续使用本地缓存:', err); setProjectSyncNotice('项目列表暂用本地缓存:无法连接服务端,新建项目可能无法在其他设备同步。'); } })(); return () => { cancelled = true; }; }, [ session, sessionValidated, allUsers, currentUser.id, currentUser.name, currentProjectId, syncProjectMembersFromServer, setProjects, setProjectSyncNotice, ]); useEffect(() => { if (!projectMembersModalOpen || !currentProjectId || !session || sessionValidated !== true) return; const project = projectsRef.current.find((item) => item.id === currentProjectId); if (!project) return; void syncProjectMembersFromServer(project.id, project.creatorId); }, [projectMembersModalOpen, currentProjectId, session, sessionValidated, syncProjectMembersFromServer, projectsRef]); useEffect(() => { if (!session || sessionValidated !== true || !currentUser.id) { setSessionSyncNotice(null); return; } const abort = new AbortController(); const epoch = sessionEpochRef.current.next(); void (async () => { try { const serverSessions = await fetchChatSessions(currentUser.id, undefined, 100, abort.signal); if (abort.signal.aborted || !sessionEpochRef.current.isCurrent(epoch)) return; const knownProjects = projectsRef.current; setSessions((prev) => { const mapped = (serverSessions ?? []).map((item) => mapServerSessionToProjectSession( item, knownProjects, prev.find((sessionItem) => sessionItem.sessionId === item.session_id) ) ); return mergeSessionLists(prev, mapped); }); setSessionSyncNotice(null); } catch (err) { if (abort.signal.aborted || !sessionEpochRef.current.isCurrent(epoch)) return; console.warn('[sessions] 服务端会话列表同步失败,继续使用本地缓存:', err); setSessionSyncNotice( '会话列表暂用本地缓存:无法连接服务端,新建会话可能无法在其他设备同步。' ); } })(); return () => { abort.abort(); }; }, [session, sessionValidated, currentUser.id, setSessions, setSessionSyncNotice, sessionEpochRef, projectsRef]); }