feat(web): wire project isolation and template cloning on project creation
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+128
-9
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user