feat(web): wire project isolation and template cloning on project creation

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WeiCHEN
2026-09-13 02:50:19 +00:00
committed by weichen
co-authored by Cursor
parent 9ca74422c9
commit 4301c034f5
+128 -9
View File
@@ -37,6 +37,7 @@ import {
resolveCardInMessages,
cancelCardInMessages,
} from './engine/cardActionEngine';
import { createProjectTemplateDeliverables } from './engine/templateFactory';
import {
loadSessions,
saveSessions,
@@ -185,6 +186,20 @@ export const INITIAL_DELIVERABLES: DeliverableItem[] = [
},
];
// 为默认第二项目 (便携式超声探头,projectId: 2) 预置全套 7 份生命周期模板副本
const P2_INITIAL = createProjectTemplateDeliverables({
projectId: 2,
projectName: '便携式掌上无线超声诊断仪',
deviceType: '便携式超声探头',
creatorId: 1,
creatorName: '张工',
});
export const ALL_INITIAL_DELIVERABLES: DeliverableItem[] = [
...INITIAL_DELIVERABLES,
...P2_INITIAL.deliverables,
];
export const MOCK_USERS: UserProfile[] = [
{
id: 1,
@@ -251,7 +266,7 @@ export default function App() {
// 全生命周期完整交付物大盘清单 (含工作流受控、项目协作共享、个人私密生成三大分类,支持持久化)
const [deliverables, setDeliverables] = useState<DeliverableItem[]>(() =>
loadDeliverables(INITIAL_DELIVERABLES)
loadDeliverables(ALL_INITIAL_DELIVERABLES)
);
// 自动持久化 deliverables
@@ -267,15 +282,26 @@ export default function App() {
const [rightsModalOpen, setRightsModalOpen] = useState(false);
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
// 获取当前激活会话的项目ID (默认为 1)
const currentProjectId = useMemo(() => {
const activeSess = sessions.find((s) => s.sessionId === activeSessionId);
return activeSess ? activeSess.projectId : 1;
}, [sessions, activeSessionId]);
// 严格按 projectId 隔离当前项目的交付物
const currentProjectDeliverables = useMemo(() => {
return deliverables.filter((doc) => doc.projectId === currentProjectId);
}, [deliverables, currentProjectId]);
// 基于当前登录用户过滤可见交付物 (私密文档严格按 creatorId 隔离,流程与共享文档全员可见)
const visibleDeliverables = useMemo(() => {
return deliverables.filter((doc) => {
return currentProjectDeliverables.filter((doc) => {
if (doc.category === 'workflow' || doc.category === 'shared') {
return true;
}
return doc.creatorId === currentUser.id;
});
}, [deliverables, currentUser.id]);
}, [currentProjectDeliverables, currentUser.id]);
// 当用户切换或可见列表变动时,自动校准激活文档
useEffect(() => {
@@ -293,7 +319,7 @@ export default function App() {
// 文档缓存与当前 AST
const [docCache, setDocCache] = useState<Record<string, DocumentAST>>(() => {
const initialCache: Record<string, DocumentAST> = {};
const initialCache: Record<string, DocumentAST> = { ...P2_INITIAL.docASTMap };
Object.entries(MOCK_DOCUMENTS).forEach(([id, meta]) => {
initialCache[id] = markdownToBlockAST(meta.markdown, {
docId: meta.docId,
@@ -314,7 +340,10 @@ export default function App() {
return initialCache;
});
const docAST = docCache[activeDocId] || docCache['doc-001'];
const docAST =
docCache[activeDocId] ||
(visibleDeliverables[0] && docCache[visibleDeliverables[0].docId]) ||
docCache['doc-001'];
// 根据当前激活文档与用户权限矩阵,计算当前生效角色
const currentDocRole: UserRole = (() => {
@@ -841,6 +870,77 @@ export default function App() {
}
}
// 3. 检查当前项目中是否存在待填报的模板副本
const matchedTemplate = currentProjectDeliverables.find((d) => {
if (!d.isTemplateOnly) return false;
if (text.includes('需求') || text.includes('摸底') || text.includes('用户')) {
return d.taskKey === 'intake' || d.taskKey === 'user_requirements';
}
if (text.includes('调研') || text.includes('竞品')) {
return d.taskKey === 'research';
}
if (text.includes('可行性') || text.includes('注册') || text.includes('报告')) {
return d.taskKey === 'feasibility_reg' || d.taskKey === 'feasibility_dev';
}
if (text.includes('设计') || text.includes('规格') || text.includes('架构')) {
return d.taskKey === 'prelim_design';
}
if (text.includes('报价') || text.includes('工时') || text.includes('预算')) {
return d.taskKey === 'cdo_quote';
}
return false;
});
if (matchedTemplate) {
const existingAst = docCache[matchedTemplate.docId];
const updatedAst: DocumentAST = {
...existingAst,
lifecycleState: 'drafting',
isClaimed: true,
summary: `智能体参谋小队已依据您的指令“${text.slice(0, 40)}...”完成深度填报与法规对齐。`,
updatedAt: new Date().toISOString().replace('T', ' ').substring(0, 16),
};
if (updatedAst.rootBlock && updatedAst.rootBlock.children) {
const firstP = updatedAst.rootBlock.children.find((b) => b.type === 'paragraph');
if (firstP) {
firstP.content += `\n\n> 🤖 **智能体填报生效**:参谋小队已结合当前项目基线与权威法规知识库完成编制,骨架模板已平滑升级为受控草稿。`;
}
}
setDeliverables((prev) =>
prev.map((d) =>
d.docId === matchedTemplate.docId
? {
...d,
isTemplateOnly: false,
lifecycleState: 'drafting',
isClaimed: true,
summary: updatedAst.summary,
updatedAt: '刚刚',
}
: d
)
);
setDocCache((prev) => ({ ...prev, [matchedTemplate.docId]: updatedAst }));
setActiveDocId(matchedTemplate.docId);
setActiveCategory('workflow');
setMessages((prev) => [
...prev,
{
id: 'ast-fill-' + Date.now(),
sessionId: activeSessionId,
role: 'assistant',
authorName: '水木东方协同参谋',
intentTag: '🎯 模板填报升级',
content: `收到!参谋小队已依据您的指令完成《${matchedTemplate.title}》的填报。\n\n该文档已成功从【📄 初始模板】平滑升格为【✍️ 编制中 (受控草稿)】,右侧工作台已为您实时呈现填报正文,请审阅。`,
timestamp: new Date().toTimeString().substring(0, 5),
},
]);
return;
}
setMessages((prev) => [
...prev,
{
@@ -1069,26 +1169,45 @@ export default function App() {
onSelectSession={setActiveSessionId}
onNewSession={() => {
const newSessId = 'sess-' + Date.now();
const newProjId = Date.now();
const newProjectTitle = '新建医疗器械研发项目';
const newDeviceType = '通用医疗器械';
const newSess: ProjectSession = {
sessionId: newSessId,
projectId: Date.now(),
title: '新建医疗器械研发项目',
deviceType: '通用医疗器械',
projectId: newProjId,
title: newProjectTitle,
deviceType: newDeviceType,
stage: 'V0.1',
stageLabel: 'V0.1 需求摸底',
messageCount: 1,
updatedAt: '刚刚',
};
const initMsg: ChatMessage = {
id: 'm-init-' + Date.now(),
sessionId: newSessId,
role: 'system',
content: '水木东方协同参谋小队已就位,当前新项目进入【V0.1 需求摸底与产品定义】阶段。请在此输入您的器械研发构想。',
content: '水木东方协同参谋小队已就位,当前新项目进入【V0.1 需求摸底与产品定义】阶段。全套研发工作流与阶段文档模板已克隆就绪,请在右侧查阅或在此输入您的研发构想。',
timestamp: new Date().toTimeString().substring(0, 5),
};
// 即时克隆全生命周期 7 份受控模板副本
const { deliverables: clonedDocs, docASTMap: clonedAsts } = createProjectTemplateDeliverables({
projectId: newProjId,
projectName: newProjectTitle,
deviceType: newDeviceType,
creatorId: currentUser.id,
creatorName: currentUser.name,
});
setSessions((prev) => [newSess, ...prev]);
setMessages((prev) => [...prev, initMsg]);
setDeliverables((prev) => [...prev, ...clonedDocs]);
setDocCache((prev) => ({ ...prev, ...clonedAsts }));
setActiveSessionId(newSessId);
setActiveDocId(clonedDocs[0].docId);
setActiveCategory('workflow');
}}
currentUser={currentUser}
currentRole={currentDocRole}