Files
Agent-System/web/src/hooks/useProjectMemberActions.ts
T
weichenandCursor 8de29466e1 refactor(web): persist project members from one hook
Refresh the roster and workflow documents only after the member write succeeds, so a failed add does not invent a local session.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-19 12:11:52 +00:00

153 lines
5.4 KiB
TypeScript

import type { Dispatch, SetStateAction } from 'react';
import {
addProjectMember,
removeProjectMember,
updateProjectMember,
} from '../api/projectMembers';
import { buildPrivateSessionId } from '../data/storage';
import { afterProjectMemberPersisted } from '../lib/afterProjectMemberPersisted';
import { buildManagedMemberUpsertPayload } from '../lib/createProjectMembersPersist';
import type {
ChatMessage,
ProjectInfo,
ProjectMember,
ProjectMemberRole,
ProjectSession,
} from '../types/workbench';
export interface ProjectMemberActionsInput {
currentProject: ProjectInfo | undefined;
syncProjectMembersFromServer: (projectId: number, creatorId: number) => Promise<unknown>;
setWorkflowDocsEpoch: Dispatch<SetStateAction<number>>;
setSessions: Dispatch<SetStateAction<ProjectSession[]>>;
setMessages: Dispatch<SetStateAction<ChatMessage[]>>;
setProjectMembersError: Dispatch<SetStateAction<string | null>>;
}
export interface ProjectMemberActions {
handleAddProjectMember: (newMember: Omit<ProjectMember, 'joinedAt' | 'status'>) => Promise<void>;
handleUpdateProjectMemberRole: (userId: number, newRole: ProjectMemberRole) => Promise<void>;
handleRemoveProjectMember: (userId: number) => Promise<void>;
handleRestoreProjectMember: (userId: number) => Promise<void>;
}
export function useProjectMemberActions(input: ProjectMemberActionsInput): ProjectMemberActions {
const {
currentProject,
syncProjectMembersFromServer,
setWorkflowDocsEpoch,
setSessions,
setMessages,
setProjectMembersError,
} = input;
const handleAddProjectMember = async (newMember: Omit<ProjectMember, 'joinedAt' | 'status'>) => {
if (!currentProject) return;
try {
await addProjectMember(
currentProject.id,
buildManagedMemberUpsertPayload(
{ userId: newMember.userId, role: newMember.role },
currentProject.creatorId,
),
);
await afterProjectMemberPersisted({
syncMembers: () => syncProjectMembersFromServer(currentProject.id, currentProject.creatorId),
refreshWorkflowDocs: () => setWorkflowDocsEpoch((n) => n + 1),
});
const newSessId = buildPrivateSessionId(`p${currentProject.id}`, newMember.userId);
setSessions((prev) => {
const exists = prev.some(
(s) => s.projectId === currentProject.id && s.userId === newMember.userId,
);
if (exists) return prev;
return [
{
sessionId: newSessId,
userId: newMember.userId,
projectId: currentProject.id,
title: currentProject.title,
deviceType: currentProject.deviceType,
stage: 'V0.1',
stageLabel: 'V0.1 需求摸底',
messageCount: 1,
updatedAt: '刚刚',
},
...prev,
];
});
setMessages((prev) => [
...prev,
{
id: `m-join-${newMember.userId}-${Date.now()}`,
sessionId: newSessId,
role: 'system',
content: `【项目协同通知】您已被项目创建人 ${currentProject.creatorName} 加入项目《${currentProject.title}》,并分配项目角色为【${newMember.role}】。`,
timestamp: new Date().toTimeString().substring(0, 5),
},
]);
} catch (err) {
setProjectMembersError(err instanceof Error ? err.message : '添加成员失败');
throw err;
}
};
const handleUpdateProjectMemberRole = async (userId: number, newRole: ProjectMemberRole) => {
if (!currentProject) return;
try {
await updateProjectMember(
currentProject.id,
buildManagedMemberUpsertPayload({ userId, role: newRole }, currentProject.creatorId),
);
await afterProjectMemberPersisted({
syncMembers: () => syncProjectMembersFromServer(currentProject.id, currentProject.creatorId),
refreshWorkflowDocs: () => setWorkflowDocsEpoch((n) => n + 1),
});
} catch (err) {
setProjectMembersError(err instanceof Error ? err.message : '更新成员角色失败');
}
};
const handleRemoveProjectMember = async (userId: number) => {
if (!currentProject) return;
try {
await removeProjectMember(currentProject.id, userId);
await afterProjectMemberPersisted({
syncMembers: () => syncProjectMembersFromServer(currentProject.id, currentProject.creatorId),
refreshWorkflowDocs: () => setWorkflowDocsEpoch((n) => n + 1),
});
} catch (err) {
setProjectMembersError(err instanceof Error ? err.message : '移出成员失败');
}
};
const handleRestoreProjectMember = async (userId: number) => {
if (!currentProject) return;
try {
const member = currentProject.members.find((m) => m.userId === userId);
await updateProjectMember(
currentProject.id,
buildManagedMemberUpsertPayload(
{ userId, role: member?.role ?? 'member', status: 'active' },
currentProject.creatorId,
),
);
await afterProjectMemberPersisted({
syncMembers: () => syncProjectMembersFromServer(currentProject.id, currentProject.creatorId),
refreshWorkflowDocs: () => setWorkflowDocsEpoch((n) => n + 1),
});
} catch (err) {
setProjectMembersError(err instanceof Error ? err.message : '恢复成员失败');
}
};
return {
handleAddProjectMember,
handleUpdateProjectMemberRole,
handleRemoveProjectMember,
handleRestoreProjectMember,
};
}