Files
Agent-System/web/src/lib/createProjectMembersPersist.ts
T
weichenandCursor 7ef34a685d fix(web): send document_duty on member add, update, and restore
Post-create role changes were silently dropping reviewer/approver
because only project_role was persisted.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 22:16:44 +00:00

113 lines
3.9 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 buildManagedMemberUpsertPayload(
{ userId: member.userId, role: member.role },
creatorUserId,
);
}
export function buildManagedMemberUpsertPayload(
input: { userId: number; role: ProjectMemberRole; status?: 'active' | 'exited' },
creatorUserId: number,
): UpsertProjectMemberPayload {
const payload: UpsertProjectMemberPayload = {
user_id: input.userId,
project_role: toApiProjectRole(input.role, input.userId, creatorUserId),
document_duty: documentDutyFromMemberRole(input.role),
};
if (input.status) {
payload.status = input.status;
}
return payload;
}
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;
}
}