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>
238 lines
9.2 KiB
TypeScript
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);
|
|
}
|
|
};
|
|
}
|