Files
Agent-System/web/src/hooks/useCreateProjectConfirm.ts
T
weichenandCursor c8dbefe8a1 fix(web): align settings CAS, PDF preview, and collaboration UX guards
Wire expected_revision, blob PDF framing, mustChangePassword session rewrite,
client_key project create, no-project send disable, and right-rail persistence.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-20 07:05:06 +00:00

238 lines
9.2 KiB
TypeScript

import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
import { upsertChatSession } from '../api/chatSessions';
import { addProjectMember } from '../api/projectMembers';
import { createProject } from '../api/projectWorkflow';
import { buildPrivateSessionId } from '../data/storage';
import { createProjectTemplateDeliverables } from '../engine/templateFactory';
import { ensureCreatedProjectWithMembers } from '../lib/createProjectMembersPersist';
import {
beginCreateProjectSubmit,
endCreateProjectSubmit,
resolveCreateProjectSubmitPhase,
shouldCloneMedicalTemplates,
type CreateProjectSubmitPhase,
} from '../lib/createProjectSubmitGuard';
import type { CreateProjectConfirmData } from '../components/project/CreateProjectModal';
import type { DocumentAST } from '../types/block';
import type { UserProfile } from '../types/docgov';
import type {
ChatMessage,
DeliverableCategory,
DeliverableItem,
ProjectInfo,
ProjectMember,
ProjectSession,
} from '../types/workbench';
export interface CreateProjectConfirmInput {
currentUser: UserProfile;
createProjectInFlightRef: MutableRefObject<boolean>;
createProjectBootstrapIdRef: MutableRefObject<number | null>;
setCreateProjectError: Dispatch<SetStateAction<string | null>>;
setCreateProjectSubmitting: Dispatch<SetStateAction<boolean>>;
setCreateProjectSubmitPhase: Dispatch<SetStateAction<CreateProjectSubmitPhase | null>>;
setCreateProjectModalOpen: Dispatch<SetStateAction<boolean>>;
setProjects: Dispatch<SetStateAction<ProjectInfo[]>>;
setSessions: Dispatch<SetStateAction<ProjectSession[]>>;
setMessages: Dispatch<SetStateAction<ChatMessage[]>>;
setDeliverables: Dispatch<SetStateAction<DeliverableItem[]>>;
setDocCache: Dispatch<SetStateAction<Record<string, DocumentAST>>>;
setActiveSessionId: Dispatch<SetStateAction<string>>;
setActiveDocId: Dispatch<SetStateAction<string>>;
setActiveCategory: Dispatch<SetStateAction<DeliverableCategory>>;
}
export function useCreateProjectConfirm(input: CreateProjectConfirmInput) {
const {
currentUser,
createProjectInFlightRef,
createProjectBootstrapIdRef,
setCreateProjectError,
setCreateProjectSubmitting,
setCreateProjectSubmitPhase,
setCreateProjectModalOpen,
setProjects,
setSessions,
setMessages,
setDeliverables,
setDocCache,
setActiveSessionId,
setActiveDocId,
setActiveCategory,
} = input;
// 发起医疗器械新项目立项 (通过 Modal 交互收集名称、品类与初始成员)
return async (data: CreateProjectConfirmData) => {
if (!beginCreateProjectSubmit(createProjectInFlightRef)) {
return;
}
setCreateProjectError(null);
setCreateProjectSubmitting(true);
setCreateProjectSubmitPhase(
resolveCreateProjectSubmitPhase(createProjectBootstrapIdRef.current),
);
let newProjId: number;
try {
try {
const ensured = await ensureCreatedProjectWithMembers({
bootstrapProjectId: createProjectBootstrapIdRef.current,
createProject: async () => {
const clientKey =
typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
? `proj-${crypto.randomUUID()}`
: `proj-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
const created = await createProject({
name: data.title,
client_key: clientKey,
device_type: data.deviceType || undefined,
intended_use: data.productDomain === 'non_medical_software' ? undefined : data.description,
product_domain: data.productDomain,
project_type_key: data.projectTypeKey,
team_mode: data.teamMode,
execution_mode: data.executionMode,
problem_statement: data.problemStatement,
target_users: data.targetUsers,
target_outcome: data.targetOutcome,
initial_workflow_key:
data.productDomain === 'non_medical_software' ? 'software_bootstrap' : undefined,
});
// create 刚成功即写入 bootstrap:成员失败窗口内串行重试不会再 create
createProjectBootstrapIdRef.current = created.id;
return created;
},
creatorUserId: currentUser.id,
initialMembers: data.initialMembers,
addMember: addProjectMember,
onPhase: setCreateProjectSubmitPhase,
});
newProjId = ensured.projectId;
createProjectBootstrapIdRef.current = ensured.projectId;
} catch (err) {
const withId = err as Error & { projectId?: number };
const bootstrapId =
typeof withId.projectId === 'number' && withId.projectId > 0
? withId.projectId
: createProjectBootstrapIdRef.current;
if (bootstrapId) {
createProjectBootstrapIdRef.current = bootstrapId;
}
const message =
err instanceof Error
? err.message
: '创建项目或同步初始成员失败,请稍后重试(不会因本地状态假成功)';
setCreateProjectError(
bootstrapId
? `项目已创建(#${bootstrapId}),但初始成员未全部落库:${message}。请再次确认以幂等补齐,勿关闭后当作成功。`
: message,
);
return;
}
const nowStr = new Date().toLocaleString();
// 1. 构建项目与成员元数据(服务端成员已持久化)
const members: ProjectMember[] = data.initialMembers.map((m) => ({
userId: m.userId,
userName: m.userName,
department: m.department,
position: m.position,
role: m.role,
status: 'active',
joinedAt: nowStr,
}));
const newProject: ProjectInfo = {
id: newProjId,
title: data.title,
deviceType: data.deviceType,
description: data.description,
creatorId: currentUser.id,
creatorName: currentUser.name,
createdAt: nowStr,
members,
};
// 2. 为每个选中的项目成员生成独立私聊会话
const newSessionsToAppend: ProjectSession[] = [];
const newMessagesToAppend: ChatMessage[] = [];
let creatorSessionId = '';
members.forEach((m) => {
const sessId = buildPrivateSessionId(`p${newProjId}`, m.userId);
if (m.userId === currentUser.id) {
creatorSessionId = sessId;
}
newSessionsToAppend.push({
sessionId: sessId,
userId: m.userId,
projectId: newProjId,
title: data.title,
deviceType: data.deviceType,
stage: 'V0.1',
stageLabel: 'V0.1 需求摸底',
messageCount: 1,
updatedAt: '刚刚',
});
newMessagesToAppend.push({
id: `m-init-${m.userId}-${Date.now()}`,
sessionId: sessId,
role: 'system',
content:
m.userId === currentUser.id
? `【立项成功】项目《${data.title}》已成功建立!全套研发工作流与阶段文档模板已克隆就绪,协同参谋小队已就位,进入【V0.1 需求摸底】阶段。您可以点击附件图标上传临床需求 URS,或直接输入您的产品构想。`
: `【项目协同通知】您已被项目创建人 ${currentUser.name} 邀请加入《${data.title}》,并分配法定职责为【${m.role}】。`,
timestamp: new Date().toTimeString().substring(0, 5),
});
});
// 3. 仅医疗项目克隆 7 份受控模板;软件项目走服务端 catalog / docs_only
const clonedDocs = shouldCloneMedicalTemplates(data.productDomain)
? createProjectTemplateDeliverables({
projectId: newProjId,
projectName: data.title,
deviceType: data.deviceType,
creatorId: currentUser.id,
creatorName: currentUser.name,
})
: { deliverables: [], docASTMap: {} };
const clonedAsts = clonedDocs.docASTMap;
setProjects((prev) => [newProject, ...prev]);
setSessions((prev) => [...newSessionsToAppend, ...prev]);
setMessages((prev) => [...prev, ...newMessagesToAppend]);
void Promise.all(
newSessionsToAppend.map((sessionItem) =>
upsertChatSession({
session_id: sessionItem.sessionId,
title: sessionItem.title,
user_id: sessionItem.userId ?? currentUser.id,
project_id: sessionItem.projectId,
})
)
).catch((err) => {
console.warn('[sessions] 服务端会话注册失败,其他设备可能暂不可见:', err);
});
setDeliverables((prev) => [...prev, ...clonedDocs.deliverables]);
setDocCache((prev) => ({ ...prev, ...clonedAsts }));
if (creatorSessionId) {
setActiveSessionId(creatorSessionId);
}
if (clonedDocs.deliverables[0]?.docId) {
setActiveDocId(clonedDocs.deliverables[0].docId);
setActiveCategory('workflow');
}
createProjectBootstrapIdRef.current = null;
setCreateProjectModalOpen(false);
setCreateProjectError(null);
} finally {
endCreateProjectSubmit(createProjectInFlightRef);
setCreateProjectSubmitting(false);
setCreateProjectSubmitPhase(null);
}
};
}