refactor(web): create projects from one hook
Remember the new project id as soon as create succeeds so a later member failure retries members instead of creating a second project. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -46,7 +46,7 @@ updated: 2026-09-19
|
||||
| FE 缓存 | done(版本字段)。窗口缓存写入 `{schema,data}`,无 schema 的旧载荷丢弃。`window cache: 无 schema` |
|
||||
| FE lucide | 关闭理由:`package-lock.json` 锁定 `lucide-react@1.42.0`,来源 `registry.npmjs.org`,带 integrity。`^1.16.0` 是合法的 1.x 范围,不是来源不明的包 |
|
||||
| M7 | done(出站意图 + 同键重放)。`document_content_projections` 与账本同一事务写入;投影成功才销账。`TestDocRevision_PatchOrder_RetryProjectsWithoutSecondLedgerCommit`。账本库与正文库仍不是一个 SQL 事务,进程退出后靠未投影行重放,不靠客户端记忆 |
|
||||
| FE App.tsx | 部分。登录壳不再静态引用重模块。本地缓存、登录同步、文档补水、服务端文档同步、生命周期、发送消息、块编辑、对话流/任务流重试和卡片动作已分别抽出。立项仍在 `App.tsx` |
|
||||
| FE App.tsx | 部分。登录壳不再静态引用重模块。本地缓存、登录同步、文档补水、服务端文档同步、生命周期、发送消息、块编辑、对话流/任务流重试、卡片动作和立项已分别抽出。工作台壳与成员管理仍在 `App.tsx` |
|
||||
| H1-del | 关闭理由:`allowLegacyTestPrincipal` 仍被 `AllowInMemory && ContextDbHost==""` 的 dev/test 内存测试使用。删除会把这批测试变成未登录 401。保留告警,删除另开、先改测试夹具 |
|
||||
|
||||
仍开放:rdev P3 三样本真 PI、首次加载 U1/U6/U7、dev 容器重建与治理库 PG。公式引擎已离开入口包(`index-j0BIrjIR.js` 737.38 kB,br 170,451 B,包内无 katex),U3 仍缺浏览器瀑布,不关。P4 键名空间化已在本分支落地:新清单用带前缀的任务键,旧键与旧 doc_id 仍可读,`TestPublishedTaskKeysAreNamespacedAndDisjoint`。意图短语仍输出旧键,执行路径按别名接上。P2 规划界面已折叠长阶段、可批量纳入人工审批,并在选中 rdev 时提示 CDO 前置。U4 登录扇出收口仍有效,浏览器请求次数证据还没有。2026-09-19 在 `fix/audit-remainder-closeout` 跑过 `cd web && npm test && npm run build`:node:test 414 pass / 0 fail,vitest 32 pass;入口 `index-BFf6n8IA.js` 1,005.01 kB,br 235,285 B。CURRENT 文首 schema 52 与三套容器一致,00053 要等重建后才升。H2 批注/审计的项目成员检查在 Postgres 上走 `RequireProjectAccess`;内存授权器在无库时把任意正用户 id 视为成员,这是 `TestAuthorizeDocRevision_ReviewerAndNonMemberCannotWrite` 的隔离前提,不能改成默认拒绝。H2 的 HTTP 负例因此要在 `TEST_DATABASE_URL` 上补,本机未设置该变量。M7 的投影意图已随账本提交(goose `00053`);容器 schema 仍是 52,要等占槽重建后才升到 53。
|
||||
|
||||
+21
-178
@@ -16,13 +16,12 @@ import {
|
||||
unshareFreeformDoc,
|
||||
deleteFreeformDoc,
|
||||
} from './api/freeformDoc';
|
||||
import { upsertChatSession } from './api/chatSessions';
|
||||
import type { DiffResult, OverlayMode } from './features/documentRevision/types';
|
||||
import {
|
||||
documentRevisionInactiveHint,
|
||||
revisionToolbarMountMode,
|
||||
} from './features/documentRevision/revisionToolbarMount';
|
||||
import { createProject, fetchProjectLineage, getProject } from './api/projectWorkflow';
|
||||
import { fetchProjectLineage, getProject } from './api/projectWorkflow';
|
||||
import {
|
||||
addProjectMember,
|
||||
listProjectMembers,
|
||||
@@ -31,14 +30,9 @@ import {
|
||||
} from './api/projectMembers';
|
||||
import {
|
||||
buildManagedMemberUpsertPayload,
|
||||
ensureCreatedProjectWithMembers,
|
||||
} from './lib/createProjectMembersPersist';
|
||||
import { afterProjectMemberPersisted } from './lib/afterProjectMemberPersisted';
|
||||
import {
|
||||
beginCreateProjectSubmit,
|
||||
endCreateProjectSubmit,
|
||||
resolveCreateProjectSubmitPhase,
|
||||
shouldCloneMedicalTemplates,
|
||||
type CreateProjectSubmitPhase,
|
||||
} from './lib/createProjectSubmitGuard';
|
||||
import { mapApiMembersToProjectMembers } from './lib/projectMemberAdapter';
|
||||
@@ -74,7 +68,7 @@ import { UserProjectRightsModal } from './components/user/UserProjectRightsModal
|
||||
import { UserSettingsModal } from './components/user/UserSettingsModal';
|
||||
import { OnDemand } from './components/lazy/OnDemand';
|
||||
import { DocVisibilityModal } from './components/doc/DocVisibilityModal';
|
||||
import { CreateProjectModal, type CreateProjectConfirmData } from './components/project/CreateProjectModal';
|
||||
import { CreateProjectModal } from './components/project/CreateProjectModal';
|
||||
import { ProjectMembersModal } from './components/project/ProjectMembersModal';
|
||||
import { ProjectSettingsModal } from './components/project/ProjectSettingsModal';
|
||||
import { fetchProjectSettings } from './api/projectSettings';
|
||||
@@ -116,7 +110,6 @@ import {
|
||||
MOCK_USERS,
|
||||
} from '@demo-seeds';
|
||||
import {
|
||||
createProjectTemplateDeliverables,
|
||||
getTemplateDefinitionByTaskKey,
|
||||
buildTemplateASTForProject,
|
||||
} from './engine/templateFactory';
|
||||
@@ -137,6 +130,7 @@ import { useAuthenticatedWorkspaceSync } from './hooks/useAuthenticatedWorkspace
|
||||
import { useBlockEditing } from './hooks/useBlockEditing';
|
||||
import { useControlledDocumentHydration } from './hooks/useControlledDocumentHydration';
|
||||
import { useDocumentLifecycleActions } from './hooks/useDocumentLifecycleActions';
|
||||
import { useCreateProjectConfirm } from './hooks/useCreateProjectConfirm';
|
||||
import { useCardActions } from './hooks/useCardActions';
|
||||
import { useTaskRunAttempts } from './hooks/useTaskRunAttempts';
|
||||
import { useSendMessage } from './hooks/useSendMessage';
|
||||
@@ -1630,6 +1624,24 @@ export default function App() {
|
||||
};
|
||||
|
||||
// 未登录拦截门禁:展示现代化医疗合规登录与注册页面
|
||||
const handleCreateProjectConfirm = useCreateProjectConfirm({
|
||||
currentUser,
|
||||
createProjectInFlightRef,
|
||||
createProjectBootstrapIdRef,
|
||||
setCreateProjectError,
|
||||
setCreateProjectSubmitting,
|
||||
setCreateProjectSubmitPhase,
|
||||
setCreateProjectModalOpen,
|
||||
setProjects,
|
||||
setSessions,
|
||||
setMessages,
|
||||
setDeliverables,
|
||||
setDocCache,
|
||||
setActiveSessionId,
|
||||
setActiveDocId,
|
||||
setActiveCategory,
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<LoginView
|
||||
@@ -1647,175 +1659,6 @@ export default function App() {
|
||||
);
|
||||
}
|
||||
|
||||
// 发起医疗器械新项目立项 (通过 Modal 交互收集名称、品类与初始成员)
|
||||
const handleCreateProjectConfirm = 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 created = await createProject({
|
||||
name: data.title,
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// 添加成员(持久化至服务端 project_members)
|
||||
const handleAddProjectMember = async (newMember: Omit<ProjectMember, 'joinedAt' | 'status'>) => {
|
||||
if (!currentProject) return;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
|
||||
test('project creation lives in one hook called before the login return', () => {
|
||||
const app = readFileSync(new URL('../App.tsx', import.meta.url), 'utf8');
|
||||
const hook = readFileSync(new URL('./useCreateProjectConfirm.ts', import.meta.url), 'utf8');
|
||||
assert.equal((app.match(/useCreateProjectConfirm\(/g) || []).length, 1);
|
||||
const callAt = app.indexOf('useCreateProjectConfirm(');
|
||||
const loginReturnAt = app.indexOf('if (!session)');
|
||||
assert.equal(callAt > 0 && callAt < loginReturnAt, true);
|
||||
assert.equal(app.includes('ensureCreatedProjectWithMembers'), false);
|
||||
assert.equal(hook.includes('create 刚成功即写入 bootstrap'), true);
|
||||
assert.equal(hook.includes('shouldCloneMedicalTemplates'), true);
|
||||
assert.equal(hook.includes('不会因本地状态假成功'), true);
|
||||
});
|
||||
@@ -0,0 +1,232 @@
|
||||
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 created = await createProject({
|
||||
name: data.title,
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user