Files
Agent-System/web/src/lib/createProjectMembersPersist.ts
T
weichenandCursor c1bdfe3920 feat(web): persist exclusive reviewer duty on project create
Keep project_role as member/viewer, send document_duty=reviewer for USER_B, and clear the seed 李主管 reviewer so the assigned reviewer is the one that syncs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 21:57:53 +00:00

99 lines
3.5 KiB
TypeScript

/**
* 立项成功后将非创建者初始成员持久化到受权 project_members API。
* 禁止仅写本地 allUsers/sessions 造成「页面勾选成功、库表无成员」假成功。
*/
import type { UpsertProjectMemberPayload } from '../api/projectMembers';
import { toApiProjectRole } from './projectMemberAdapter';
import { documentDutyFromMemberRole } from './projectDocumentDuty';
import type { ProjectMemberRole } from '../types/workbench';
export type InitialProjectMemberInput = {
userId: number;
userName: string;
department?: string;
position?: string;
role: ProjectMemberRole;
};
export function selectNonCreatorInitialMembers(
initialMembers: InitialProjectMemberInput[],
creatorUserId: number,
): InitialProjectMemberInput[] {
return initialMembers.filter((m) => m.userId !== creatorUserId);
}
export function buildInitialMemberUpsertPayload(
member: InitialProjectMemberInput,
creatorUserId: number,
): UpsertProjectMemberPayload {
return {
user_id: member.userId,
project_role: toApiProjectRole(member.role, member.userId, creatorUserId),
document_duty: documentDutyFromMemberRole(member.role),
};
}
export type AddProjectMemberFn = (
projectId: number,
payload: UpsertProjectMemberPayload,
) => Promise<void>;
/**
* 逐一 upsert 非创建者;任一失败抛错(调用方保留已创建 projectId 以便重试幂等)。
* Upsert 语义下重复确认不会因已存在成员而假失败。
*/
export async function persistInitialProjectMembers(params: {
projectId: number;
creatorUserId: number;
initialMembers: InitialProjectMemberInput[];
addMember: AddProjectMemberFn;
}): Promise<{ persistedUserIds: number[] }> {
const targets = selectNonCreatorInitialMembers(params.initialMembers, params.creatorUserId);
const persistedUserIds: number[] = [];
for (const member of targets) {
const payload = buildInitialMemberUpsertPayload(member, params.creatorUserId);
await params.addMember(params.projectId, payload);
persistedUserIds.push(member.userId);
}
return { persistedUserIds };
}
export type CreateProjectEnsurePhase = 'creating' | 'syncing_members';
/**
* 若已有 bootstrapProjectId(上次 create 成功但成员失败),跳过再次 createProject,仅补成员。
*/
export async function ensureCreatedProjectWithMembers(params: {
bootstrapProjectId: number | null;
createProject: () => Promise<{ id: number }>;
creatorUserId: number;
initialMembers: InitialProjectMemberInput[];
addMember: AddProjectMemberFn;
/** 可选:驱动 UI「立项中 / 同步成员中」文案(不参与控制流)。 */
onPhase?: (phase: CreateProjectEnsurePhase) => void;
}): Promise<{ projectId: number; createdNow: boolean; persistedUserIds: number[] }> {
let projectId = params.bootstrapProjectId;
let createdNow = false;
if (projectId == null || projectId <= 0) {
params.onPhase?.('creating');
const created = await params.createProject();
projectId = created.id;
createdNow = true;
}
params.onPhase?.('syncing_members');
try {
const { persistedUserIds } = await persistInitialProjectMembers({
projectId,
creatorUserId: params.creatorUserId,
initialMembers: params.initialMembers,
addMember: params.addMember,
});
return { projectId, createdNow, persistedUserIds };
} catch (err) {
const wrapped = err instanceof Error ? err : new Error(String(err));
(wrapped as Error & { projectId?: number }).projectId = projectId;
throw wrapped;
}
}