Files
Agent-System/web/src/hooks/useAuthenticatedWorkspaceSync.ts
T
WeiCHEN 6d3efdf4d7 feat(auth): show real names in duty pickers and enable profile edits
Add a login-visible user directory API and admin/self profile update
paths so project duty selectors stop falling back to "用户 N", and so
users can persist display fields with an admin-edit tip.
2026-09-23 09:39:41 +08:00

238 lines
8.7 KiB
TypeScript

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<SetStateAction<UserProfile[]>>;
setCurrentUser: Dispatch<SetStateAction<UserProfile>>;
setSession: Dispatch<SetStateAction<LoginResult | null>>;
setSessionValidated: Dispatch<SetStateAction<boolean | null>>;
setSystemEnv: Dispatch<SetStateAction<string>>;
setSystemAuthConfig: Dispatch<SetStateAction<SystemAuthConfig>>;
setCapabilities: Dispatch<SetStateAction<string[]>>;
session: LoginResult | null;
sessionValidated: boolean | null;
currentUser: UserProfile;
currentProjectId: number;
syncProjectMembersFromServer: (projectId: number, creatorId: number) => Promise<unknown>;
setProjects: Dispatch<SetStateAction<ProjectInfo[]>>;
setProjectSyncNotice: Dispatch<SetStateAction<string | null>>;
projectMembersModalOpen: boolean;
projectsRef: MutableRefObject<ProjectInfo[]>;
setSessions: Dispatch<SetStateAction<ProjectSession[]>>;
setSessionSyncNotice: Dispatch<SetStateAction<string | null>>;
sessionEpochRef: MutableRefObject<RequestEpoch>;
}
/**
* 已登录工作台的启动同步:校验会话、拉项目、按需同步当前项目成员、拉会话列表。
* 首次校验故意只跑一次,沿用入口组件原来的空依赖,避免用户目录更新把登录态重放一遍。
*/
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]);
}