🔒 [隐私受控标记] 本文档为部门主管内部管理草稿,仅李主管可见。
+`,
+ },
+
+ 'doc-cusa-intake': {
+ docId: 'doc-cusa-intake',
+ projectId: 3,
+ taskKey: 'intake',
+ title: '数字化高频超声手术吸引系统立项需求与产品定义',
+ stage: 'V0.1',
+ category: 'workflow',
+ creatorId: 1,
+ creatorName: '张工',
+ version: 'V1.0',
+ lifecycleState: 'approved',
+ isClaimed: true,
+ author: '张工',
+ reviewer: '李主管',
+ approver: '王总监',
+ updatedAt: '2026-09-08 17:00',
+ summary: '高频超声手术吸引系统 (CUSA-Ultra) 初始需求摸底、预期临床用途与整机工程规格基线。',
+ markdown: `# 数字化高频超声手术吸引系统 (CUSA-Ultra) 立项需求与产品定义
+
+## 1. 临床背景与预期用途
+超声手术吸引系统(CUSA)利用压电换能器驱动钛合金空心刀头产生高频纵向振动(24kHz ~ 55kHz),在组织间隙形成空化微爆破效应,选择性乳化富含水分的实质性脏器细胞(如肿瘤及肝实质),同时完整保留富含胶原纤维的血管与神经鞘膜。结合同轴蠕动冲洗与高负压吸引,实现术野无血、精细组织剥离与清除。
+
+## 2. 核心技术指标基线
+- **工作频率**:低频大振幅挡位 24.0 kHz ± 0.5 kHz;高频精细挡位 55.0 kHz ± 1.0 kHz。
+- **刀头材质**:医用钛合金 TC4 ELI(Ti-6Al-4V ELI,符合 ASTM F136 / GB/T 13810)。
+- **冲洗灌注系统**:精密蠕动泵控制,流量范围 5 ~ 50 mL/min。
+- **吸引系统**:内建静音无油吸引泵,负压调节范围 -20 kPa ~ -85 kPa。
+- **控制方式**:双脚踏双挡开关(切割振动挡 + 强力冲洗吸引挡)。
+`,
+ },
+
+ 'doc-cusa-feasibility-reg': {
+ docId: 'doc-cusa-feasibility-reg',
+ projectId: 3,
+ taskKey: 'feasibility_reg',
+ title: '数字化高频超声手术吸引系统立项与可行性法规论证报告',
+ stage: 'V0.5',
+ category: 'workflow',
+ creatorId: 1,
+ creatorName: '张工',
+ version: 'V1.0',
+ lifecycleState: 'approved',
+ isClaimed: true,
+ author: '张工',
+ reviewer: '李主管',
+ approver: '王总监',
+ updatedAt: '2026-09-08 17:30',
+ summary: '针对 CUSA 超声吸引刀头与主机的三类医疗器械分类准入、注册检验与临床评价路径全景评估。',
+ markdown: `# 数字化高频超声手术吸引系统 (CUSA-Ultra) 立项与可行性法规论证报告
+
+## 1. 监管分类界定与强标合规矩阵
+依据国家药品监督管理局《医疗器械分类目录》(子目录 01-有源手术器械,类别 01-01 超声手术设备及附件),本项目整机主机、换能手柄及吸引刀头属于**第三类医疗器械**管理(涉及神经外科重要血管及中枢神经深部解剖)。
+
+### 强制性标准符合性矩阵
+| 标准编号 | 标准名称 | 适用范围与要求 | 拟采取工程防护对策 |
+| :--- | :--- | :--- | :--- |
+| **GB 9706.1-2020** | 医用电气设备 第1部分:基本安全与基本性能通用要求 | CF型应用部分,双重隔离患者回路 | 隔离变压器 + 漏电流实时监测闭环 |
+| **GB 9706.202-2021** | 高频手术设备及超声手术设备专用要求 | 能量输出限制与空化振幅过载保护 | 锁相环(PLL)自适应频率与振幅闭环控制 |
+| **YY 0505-2012** | 医用电气设备 第1-2部分:电磁兼容要求和试验 | 传导与辐射骚扰限值(B组) | 主板多级屏蔽罩、滤波输入插座与接地环 |
+| **GB/T 16886.1-2022** | 医疗器械生物相容性风险评定 | 刀头持久接触软组织与循环血液 | 细胞毒性、致敏、皮内反应、热原与溶血试验 |
+
+## 2. 临床评价策略路径
+建议采用《医疗器械临床评价技术指导原则》中**同品种医疗器械比对路径**。
+- **主对标产品**:Integra LifeSciences CUSA® Excel / CUSA® Clarity(已获 NMPA 三类进口注册证)。
+- **等同性论证**:在工作频率(24kHz)、空化振幅(100~300μm)、刀头材料(TC4 ELI)及吸引负压范围实现等同,通过实验室台架物理比对试验与离体动物肝脏切割效能测试代替大规模临床试验。
+`,
+ },
+
+ 'doc-cusa-free-cavitation': {
+ docId: 'doc-cusa-free-cavitation',
+ projectId: 3,
+ taskKey: 'free_cusa_cavitation_risk',
+ title: '24kHz高频空化钛合金刀头微裂纹与疲劳剥落风险专项分析报告',
+ stage: 'V0.5',
+ category: 'shared',
+ creatorId: 1,
+ creatorName: '张工',
+ version: 'V1.0',
+ lifecycleState: 'approved',
+ isClaimed: true,
+ author: '张工',
+ reviewer: '李主管',
+ approver: '王总监',
+ updatedAt: '2026-09-08 17:15',
+ summary: '自由任务产出:基于超声高频交变应力与空化微射流冲击的钛合金刀头疲劳寿命与表面钝化防护策略。',
+ markdown: `# 24kHz高频空化钛合金刀头微裂纹与疲劳剥落风险专项分析报告
+
+## 1. 任务背景与物理失效机制
+超声外科刀头在 $f = 24.0\\,\\text{kHz}$ 工作时,每秒承受 24000 次微幅交变拉压循环(高周疲劳 $N > 10^7$)。同时,刀尖液体介质中剧烈崩溃的空化气泡产生局部瞬时压力高达 $1000\\,\\text{MPa}$ 的微射流冲击,容易导致刀头前端产生空化微孔、剥落并诱发微裂纹扩展。
+
+## 2. 疲劳应力分析与防护方案
+依据材料疲劳交变应力模型:
+$$
+\\sigma_a = \\sigma_f' (2N_f)^b + E \\varepsilon_f' (2N_f)^c
+$$
+- **选材强化**:必须严格采购航天级超低间隙 **TC4 ELI (Ti-6Al-4V ELI)** 锻件,间隙氧含量严格控制在 $O \\le 0.13\\%$,大幅提升冲击断裂韧度 $K_{IC}$。
+- **表面改性工艺**:采用微细丸喷丸(Shot Peening)在刀头表面引入 200~300 MPa 的残余压应力层,中和高频交变拉应力;结合微弧氧化形成致密金红石型 $\\text{TiO}_2$ 陶瓷相耐空化冲击硬化层。
+- **刀头寿命终点监控**:主机内部阻抗谱分析算法,实时监测谐振品质因数 $Q$ 值。当发生微裂纹时谐振频移 $> 300\\,\\text{Hz}$,主机毫秒级停振报警,保障手术绝对安全。
+`,
+ },
+
+ 'doc-cusa-quote': {
+ docId: 'doc-cusa-quote',
+ projectId: 3,
+ taskKey: 'cdo_quote',
+ title: 'CDO超声手术系统工程样机研发与型检预算投资核算单',
+ stage: 'V1.0',
+ category: 'workflow',
+ creatorId: 1,
+ creatorName: '张工',
+ version: 'V1.0',
+ lifecycleState: 'drafting',
+ isClaimed: true,
+ author: '张工',
+ reviewer: '李主管',
+ approver: '王总监',
+ updatedAt: '2026-09-08 17:45',
+ summary: 'CDO 商业立项核算清单:高频超声驱动电源、压电换能器、钛合金刀头模具及三类型检规费。',
+ markdown: `# CDO 超声手术吸引系统工程样机研发与型检预算投资核算单
+
+## 1. 阶段预算分解表
+依据 CDO 研发商业模型与物料清单,样机研发及检测总投入测算如下:
+
+| 研发阶段 / 项目 | 核心任务与物料构成 | 预算估算 (万元) | 计划周期 |
+| :--- | :--- | :--- | :--- |
+| **阶段 1:电源与声学驱动研发** | 24kHz/55kHz 双频锁相环电路、全桥逆变拓扑、FPGA 阻抗跟踪算法 | 28.50 | 3.5 个月 |
+| **阶段 2:换能手柄与刀头试制** | PZT-8 压电陶瓷堆栈、TC4 ELI 刀头微弧氧化与疲劳台架验证 | 19.80 | 2.5 个月 |
+| **阶段 3:冲洗吸引整机集成** | 双通道无油吸引泵、蠕动注水泵、阻燃医用脚踏开关开模 | 16.20 | 2.0 个月 |
+| **阶段 4:三类医疗器械型式检验** | GB 9706.1-2020、GB 9706.202、YY 0505 电磁兼容检验规费与整改 | 24.00 | 4.0 个月 |
+| **总计估算** | **全生命周期 CDO 首期工程样机转产投资** | **88.50 万元** | **12 个月** |
`,
},
};
diff --git a/web/src/data/storage.test.ts b/web/src/data/storage.test.ts
index 30494f8c..6cef8308 100644
--- a/web/src/data/storage.test.ts
+++ b/web/src/data/storage.test.ts
@@ -1,81 +1,136 @@
import test from 'node:test';
import assert from 'node:assert/strict';
-import { DEFAULT_MESSAGES, DEFAULT_SESSIONS } from './storage';
+import {
+ DEFAULT_MESSAGES,
+ DEFAULT_SESSIONS,
+ buildPrivateSessionId,
+} from './storage';
+import {
+ filterSessionsForUser,
+ resolveActiveSessionForUser,
+ ensurePrivateSessionsForUser,
+ assertNoCrossUserChatLeak,
+} from './sessionPrivacy';
import { ChatMessage } from '../types/workbench';
-test('多项目会话消息隔离测试 (Multi-project Session Messages Strict Isolation)', () => {
- // 1. 验证预设会话消息按 sessionId 严格分离
- const sess1Msgs = DEFAULT_MESSAGES.filter((m) => m.sessionId === 'sess-001');
- const sess2Msgs = DEFAULT_MESSAGES.filter((m) => m.sessionId === 'sess-002');
+test('每人独立私聊:同一项目不同用户拥有不同 sessionId', () => {
+ const zhang = DEFAULT_SESSIONS.filter((s) => s.userId === 1 && s.projectId === 3);
+ const li = DEFAULT_SESSIONS.filter((s) => s.userId === 2 && s.projectId === 3);
+ const wang = DEFAULT_SESSIONS.filter((s) => s.userId === 3 && s.projectId === 3);
- assert.ok(sess1Msgs.length >= 2, 'sess-001 应当包含其专属消息');
- assert.ok(sess2Msgs.length >= 2, 'sess-002 应当包含其专属消息');
+ assert.equal(zhang.length, 1);
+ assert.equal(li.length, 1);
+ assert.equal(wang.length, 1);
+ assert.equal(zhang[0].sessionId, buildPrivateSessionId('cusa', 1));
+ assert.equal(li[0].sessionId, buildPrivateSessionId('cusa', 2));
+ assert.equal(wang[0].sessionId, buildPrivateSessionId('cusa', 3));
+ assert.notEqual(zhang[0].sessionId, li[0].sessionId);
+});
+
+test('左侧会话列表按当前用户过滤,不可见他人私聊', () => {
+ const zhangView = filterSessionsForUser(DEFAULT_SESSIONS, 1);
+ const liView = filterSessionsForUser(DEFAULT_SESSIONS, 2);
+
+ assert.ok(zhangView.every((s) => s.userId === 1));
+ assert.ok(liView.every((s) => s.userId === 2));
+ assert.ok(!zhangView.some((s) => s.sessionId === buildPrivateSessionId('cusa', 2)));
+ assert.ok(!liView.some((s) => s.sessionId === buildPrivateSessionId('cusa', 1)));
+});
+
+test('消息严格归属各自私聊 session,无跨用户混聊', () => {
+ const zhangSess = buildPrivateSessionId('cusa', 1);
+ const liSess = buildPrivateSessionId('cusa', 2);
+ const wangSess = buildPrivateSessionId('cusa', 3);
+
+ const zhangMsgs = DEFAULT_MESSAGES.filter((m) => m.sessionId === zhangSess);
+ const liMsgs = DEFAULT_MESSAGES.filter((m) => m.sessionId === liSess);
+ const wangMsgs = DEFAULT_MESSAGES.filter((m) => m.sessionId === wangSess);
+
+ assert.ok(zhangMsgs.length >= 4, '张工私聊应有实质对话');
+ assert.ok(liMsgs.length >= 4, '李主管私聊应有实质对话');
+ assert.ok(wangMsgs.length >= 2, '王总监私聊应有实质对话');
+
+ // 张工窗口不得出现李主管/王总监作为 user 发言人
+ assert.ok(!zhangMsgs.some((m) => m.role === 'user' && m.authorName === '李主管'));
+ assert.ok(!zhangMsgs.some((m) => m.role === 'user' && m.authorName === '王总监'));
+ // 李主管窗口不得出现张工/王总监 user 发言
+ assert.ok(!liMsgs.some((m) => m.role === 'user' && m.authorName === '张工'));
+ assert.ok(!liMsgs.some((m) => m.role === 'user' && m.authorName === '王总监'));
+ // 王总监窗口不得出现张工/李主管 user 发言
+ assert.ok(!wangMsgs.some((m) => m.role === 'user' && m.authorName === '张工'));
+ assert.ok(!wangMsgs.some((m) => m.role === 'user' && m.authorName === '李主管'));
+
+ const leak = assertNoCrossUserChatLeak(DEFAULT_SESSIONS, DEFAULT_MESSAGES);
+ assert.equal(leak.ok, true, `跨用户泄漏: ${leak.leaks.join('; ')}`);
+});
+
+test('切换用户时 resolve 到该用户同项目私聊会话', () => {
+ const next = resolveActiveSessionForUser(
+ DEFAULT_SESSIONS,
+ 2,
+ 3,
+ buildPrivateSessionId('cusa', 1)
+ );
+ assert.equal(next, buildPrivateSessionId('cusa', 2));
+});
+
+test('resolve 优先有消息的私聊,跳过空壳 sess-p{N}-u{id}', () => {
+ const shell = {
+ sessionId: 'sess-p3-u1',
+ userId: 1,
+ projectId: 3,
+ title: '数字化超声手术吸引系统 (CUSA-Ultra)',
+ deviceType: '高频超声手术吸引切割系统',
+ stage: 'V0.5' as const,
+ stageLabel: 'V0.5',
+ messageCount: 0,
+ updatedAt: '刚刚',
+ };
+ const mixed = [shell, ...DEFAULT_SESSIONS];
+ const counts: Record
= {};
+ for (const m of DEFAULT_MESSAGES) {
+ counts[m.sessionId] = (counts[m.sessionId] || 0) + 1;
+ }
+ const next = resolveActiveSessionForUser(mixed, 1, 3, 'sess-p3-u1', counts);
+ assert.equal(next, buildPrivateSessionId('cusa', 1));
+});
+
+test('ensurePrivateSessionsForUser 为缺失用户补齐空私聊', () => {
+ const onlyZhang = DEFAULT_SESSIONS.filter((s) => s.userId === 1);
+ const ensured = ensurePrivateSessionsForUser(onlyZhang, 2);
+ const liCusa = ensured.find((s) => s.userId === 2 && s.projectId === 3);
+ assert.ok(liCusa, '应为李主管补齐 CUSA 私聊会话');
+ assert.equal(liCusa!.messageCount, 0);
+});
+
+test('多项目会话消息隔离测试仍成立(endo vs ultrasound)', () => {
+ const endo = buildPrivateSessionId('endo', 1);
+ const ultra = buildPrivateSessionId('ultrasound', 1);
+ const sess1Msgs = DEFAULT_MESSAGES.filter((m) => m.sessionId === endo);
+ const sess2Msgs = DEFAULT_MESSAGES.filter((m) => m.sessionId === ultra);
+
+ assert.ok(sess1Msgs.length >= 2);
+ assert.ok(sess2Msgs.length >= 2);
- // 验证 sess-001 包含内窥镜关键字,不包含超声探头
const sess1Combined = sess1Msgs.map((m) => m.content).join(' ');
assert.ok(sess1Combined.includes('荧光内窥镜'));
assert.ok(!sess1Combined.includes('超声声输出安全标准'));
- // 验证 sess-002 包含超声探头关键字,不包含腹腔镜初稿
const sess2Combined = sess2Msgs.map((m) => m.content).join(' ');
assert.ok(sess2Combined.includes('便携式掌上无线超声诊断仪'));
assert.ok(!sess2Combined.includes('4K超高清荧光内窥镜系统立项与可行性报告'));
- // 2. 模拟向 sess-001 追加一条新消息
const allMessages: ChatMessage[] = [...DEFAULT_MESSAGES];
- const newMsgSess1: ChatMessage = {
+ allMessages.push({
id: 'm-new-1',
- sessionId: 'sess-001',
+ sessionId: endo,
role: 'user',
+ authorName: '张工',
content: '请测算一下双棱镜分光成本',
timestamp: '14:30',
- };
- allMessages.push(newMsgSess1);
+ });
- // 3. 验证此时切到 sess-002 时,绝不出现新消息
- const filteredSess2 = allMessages.filter((m) => m.sessionId === 'sess-002');
- assert.equal(filteredSess2.length, sess2Msgs.length, 'sess-002 的消息条数应当完全不受 sess-001 变动的影响');
+ const filteredSess2 = allMessages.filter((m) => m.sessionId === ultra);
+ assert.equal(filteredSess2.length, sess2Msgs.length);
assert.ok(!filteredSess2.some((m) => m.content.includes('双棱镜分光成本')));
-
- // 4. 验证此时切到 sess-001 时,准确多了一条消息
- const filteredSess1 = allMessages.filter((m) => m.sessionId === 'sess-001');
- assert.equal(filteredSess1.length, sess1Msgs.length + 1);
- assert.ok(filteredSess1.some((m) => m.content.includes('双棱镜分光成本')));
-});
-
-test('新建项目会话独立初始化隔离测试 (New Session Spawning & Isolation)', () => {
- const allSessions = [...DEFAULT_SESSIONS];
- const newSessId = 'sess-new-999';
-
- const newSess = {
- sessionId: newSessId,
- projectId: 999,
- title: '神经外科双极电凝系统',
- deviceType: '高频手术设备',
- stage: 'V0.1' as const,
- stageLabel: 'V0.1 需求摸底',
- messageCount: 1,
- updatedAt: '刚刚',
- };
- allSessions.unshift(newSess);
-
- const initialMsgs: ChatMessage[] = [
- ...DEFAULT_MESSAGES,
- {
- id: 'm-init-999',
- sessionId: newSessId,
- role: 'system',
- content: '水木东方协同参谋小队已就位,当前项目【神经外科双极电凝系统】进入需求摸底阶段。',
- timestamp: '刚刚',
- },
- ];
-
- // 检查新项目消息隔离
- const sess999Msgs = initialMsgs.filter((m) => m.sessionId === newSessId);
- assert.equal(sess999Msgs.length, 1);
- assert.ok(sess999Msgs[0].content.includes('神经外科双极电凝系统'));
-
- // 检查原项目消息数量未变
- assert.equal(initialMsgs.filter((m) => m.sessionId === 'sess-001').length, 2);
- assert.equal(initialMsgs.filter((m) => m.sessionId === 'sess-002').length, 2);
});
diff --git a/web/src/data/storage.ts b/web/src/data/storage.ts
index 47658e2f..5c8c7b57 100644
--- a/web/src/data/storage.ts
+++ b/web/src/data/storage.ts
@@ -1,8 +1,11 @@
import { ProjectSession, ChatMessage, DeliverableItem } from '../types/workbench';
import { RoleDefinition } from '../types/rbac';
import { UserProfile } from '../types/docgov';
+import { buildPrivateSessionId, dedupePrivateSessions } from './sessionPrivacy';
+import { NEUROPULSE_SESSIONS, NEUROPULSE_MESSAGES, NEUROPULSE_ACTIVE_SESSION_ID } from './neuropulseDemo';
-const STORAGE_PREFIX = 'smdf_agent_workbench_';
+// v6:修正 NeuroPulse 驳回追问误挂「疲劳寿命」旧答复;对话 LLM 每轮独立工作区
+const STORAGE_PREFIX = 'smdf_agent_workbench_v6_';
export const STORAGE_KEYS = {
SESSIONS: `${STORAGE_PREFIX}sessions`,
@@ -14,12 +17,58 @@ export const STORAGE_KEYS = {
USERS: `${STORAGE_PREFIX}users`,
};
+export {
+ buildPrivateSessionId,
+ filterSessionsForUser,
+ resolveActiveSessionForUser,
+ ensurePrivateSessionsForUser,
+ dedupePrivateSessions,
+} from './sessionPrivacy';
+
/**
- * 默认预设项目会话
+ * 默认预设项目会话(每人独立私聊:同一项目不同 userId 各自一条 session)
+ * NeuroPulse (5201) 置顶:全流程实测展示项目
*/
export const DEFAULT_SESSIONS: ProjectSession[] = [
+ ...NEUROPULSE_SESSIONS,
+ // CUSA-Ultra:张工 / 李主管 / 王总监 各一条私聊
{
- sessionId: 'sess-001',
+ sessionId: buildPrivateSessionId('cusa', 1),
+ userId: 1,
+ projectId: 3,
+ title: '数字化超声手术吸引系统 (CUSA-Ultra)',
+ deviceType: '高频超声手术吸引切割系统',
+ stage: 'V0.5',
+ stageLabel: 'V0.5 方案成型与法规对齐',
+ messageCount: 12,
+ updatedAt: '刚刚',
+ },
+ {
+ sessionId: buildPrivateSessionId('cusa', 2),
+ userId: 2,
+ projectId: 3,
+ title: '数字化超声手术吸引系统 (CUSA-Ultra)',
+ deviceType: '高频超声手术吸引切割系统',
+ stage: 'V0.5',
+ stageLabel: 'V0.5 方案成型与法规对齐',
+ messageCount: 8,
+ updatedAt: '刚刚',
+ },
+ {
+ sessionId: buildPrivateSessionId('cusa', 3),
+ userId: 3,
+ projectId: 3,
+ title: '数字化超声手术吸引系统 (CUSA-Ultra)',
+ deviceType: '高频超声手术吸引切割系统',
+ stage: 'V0.5',
+ stageLabel: 'V0.5 方案成型与法规对齐',
+ messageCount: 4,
+ updatedAt: '刚刚',
+ },
+ // 其他项目默认归属张工私聊
+ {
+ sessionId: buildPrivateSessionId('endo', 1),
+ userId: 1,
projectId: 1,
title: '4K超高清荧光内窥镜系统',
deviceType: '硬管荧光腹腔内窥镜',
@@ -29,7 +78,8 @@ export const DEFAULT_SESSIONS: ProjectSession[] = [
updatedAt: '14:20',
},
{
- sessionId: 'sess-002',
+ sessionId: buildPrivateSessionId('ultrasound', 1),
+ userId: 1,
projectId: 2,
title: '便携式掌上无线超声诊断仪',
deviceType: '便携式超声探头',
@@ -44,22 +94,293 @@ export const DEFAULT_SESSIONS: ProjectSession[] = [
* 默认预设消息流(按 sessionId 严格隔离)
*/
export const DEFAULT_MESSAGES: ChatMessage[] = [
- // sess-001 专属对话 (4K超高清荧光内窥镜)
+ ...NEUROPULSE_MESSAGES,
+ // ==================================================================================
+ // CUSA-Ultra:按用户拆分私聊(sess-cusa-u1/u2/u3),严禁多角色混聊
+ // ==================================================================================
+ {
+ id: 'm-cusa-0',
+ sessionId: buildPrivateSessionId('cusa', 1),
+ role: 'system',
+ content: '项目助手已就位,当前项目【数字化高频超声手术吸引系统 (CUSA-Ultra)】进入【V0.5 方案成型与法规对齐】阶段。全生命周期法定责任人已指定:编制人 张工、审核人 李主管、批准人 王总监。',
+ timestamp: '16:00',
+ },
+ {
+ id: 'm-cusa-1',
+ sessionId: buildPrivateSessionId('cusa', 1),
+ role: 'assistant',
+ authorName: '项目助手',
+ intentTag: '🎯 项目初始画像与责任人就绪',
+ content:
+ '张工您好!【数字化超声手术吸引系统 (CUSA-Ultra)】多方协同工作空间已初始化完毕。本系统定位于微创外科实质脏器(脑组织、肝胆等)的微创精细乳化与同轴吸引。项目助手已备好法规知识库、声学仿真模型与质量门禁引擎,随时协助团队推进研发!',
+ timestamp: '16:01',
+ phaseEvents: [
+ { step: '1', icon: '👥', label: '组装小队', status: 'completed' },
+ { step: '2', icon: '📋', label: '角色配置', status: 'completed' },
+ { step: '3', icon: '🔬', label: '声学建模', status: 'completed' },
+ { step: '4', icon: '🛡️', label: '门禁待命', status: 'completed' },
+ ],
+ },
+ {
+ id: 'm-cusa-2',
+ sessionId: buildPrivateSessionId('cusa', 1),
+ role: 'user',
+ authorName: '张工',
+ content:
+ '项目助手,新项目【数字化超声手术吸引系统】已经立项,请简要介绍目前针对三类超声外科设备的监管要点与关键工程参数。',
+ timestamp: '16:03',
+ },
+ {
+ id: 'm-cusa-3',
+ sessionId: buildPrivateSessionId('cusa', 1),
+ role: 'assistant',
+ authorName: '项目助手',
+ intentTag: '📚 监管法规与核心参数咨询',
+ content:
+ '收到张工的立项咨询!针对超声外科手术吸引设备(CUSA),核心监管与工程要点如下:\n\n1. **监管分类**:属于国家药监局 **第三类有源手术器械**(分类编码:01-01-01 超声手术设备及附件),因其直接接触硬脑膜深层、脑实质或肝门静脉等高危血管神经区域。\n2. **关键工程参数**:\n - **谐振工作频率**:通常采用 **24 kHz ± 0.5 kHz**(低频高振幅,适合组织强力粉碎)与 **55 kHz ± 1.0 kHz**(高频微振幅,适合神经近端精细剥离)双频架构;\n - **刀头材质**:推荐选用航空医用级 **TC4 ELI(Ti-6Al-4V ELI)** 钛合金棒材,抗疲劳极限必须 $\\ge 500\\text{ MPa}$;\n - **同轴冲吸引**:冲洗流速需保持 5~50 mL/min 精准可控,吸引负压需达到 -20 kPa ~ -85 kPa 快速排空。\n3. **强制性标准矩阵**:整机需通过 GB 9706.1-2020、GB 9706.202-2021(超声手术专用强标)以及 YY 0505 电磁兼容检验。',
+ timestamp: '16:04',
+ },
+ {
+ id: 'm-cusa-4',
+ sessionId: buildPrivateSessionId('cusa', 1),
+ role: 'user',
+ authorName: '张工',
+ content:
+ '收到!另外,研发团队今晚要赶方案,推荐几款清爽低糖又提神的健康加班茶饮或轻食吧!',
+ timestamp: '16:06',
+ },
+ {
+ id: 'm-cusa-5',
+ sessionId: buildPrivateSessionId('cusa', 1),
+ role: 'assistant',
+ authorName: '项目助手',
+ intentTag: '☕ 生活调剂与健康关怀',
+ content:
+ '哈哈,研发攻关非常辛苦,特别理解!为您和团队精选几款兼具提神与护眼的健康加班搭档:\n\n1. **清爽提神茶饮**:\n - **薄荷柠檬绿茶(无糖)**:天然薄荷醇激活大脑神经突触反应,茶多酚平缓释放咖啡因,没有心悸顿挫感;\n - **罗汉果金银花菊花饮**:微甜来自罗汉果苷(0糖0卡),清热润喉,特别适合长时间讨论方案;\n - **冷萃黑咖啡兑电解质气泡水**:瞬时提神补充水分,无糖负担。\n2. **健康低GI轻食**:\n - 原味混合坚果(巴旦木、核桃,补充不饱和脂肪酸);\n - 即食低脂鸡胸肉干或无糖高蛋白酸奶,避免高血糖波动造成的饭后困倦。\n\n💡 小提示:久坐每隔 1 小时记得活动颈椎并眺望远方 5 分钟。',
+ timestamp: '16:07',
+ },
+ {
+ id: 'm-cusa-6',
+ sessionId: buildPrivateSessionId('cusa', 1),
+ role: 'user',
+ authorName: '张工',
+ content:
+ '多谢!工程参数已敲定:双频 24.0kHz/55.0kHz,刀头 TC4 ELI 航天级棒材,吸引负压 -20kPa~-85kPa 无极可调,冲洗流速 5-50 mL/min。请将其录入黑板事实。',
+ timestamp: '16:10',
+ },
+ {
+ id: 'm-cusa-7',
+ sessionId: buildPrivateSessionId('cusa', 1),
+ role: 'assistant',
+ authorName: '项目助手',
+ intentTag: '📝 编制人补充关键工程事实',
+ content:
+ '已将编制负责人【张工】补充的关键技术规格精准录入项目多方黑板事实库!\n- `frequency`: 24.0 kHz / 55.0 kHz (双频自适应切换)\n- `horn_material`: TC4 ELI (Ti-6Al-4V ELI 航天级超低间隙锻件)\n- `suction_pressure`: -20 kPa ~ -85 kPa (无极步进调压)\n- `irrigation_rate`: 5 ~ 50 mL/min (闭环蠕动控制)\n\n相关工程数据已自动注入立项可行性评估矩阵。',
+ timestamp: '16:11',
+ phaseEvents: [
+ { step: '1', icon: '📝', label: '事实解析', status: 'completed' },
+ { step: '2', icon: '💾', label: '黑板落盘', status: 'completed' },
+ { step: '3', icon: '🔄', label: '上下文注入', status: 'completed' },
+ ],
+ },
+
+ // --- 李主管私聊会话开场 ---
+ {
+ id: 'm-cusa-li-0',
+ sessionId: buildPrivateSessionId('cusa', 2),
+ role: 'system',
+ content: '项目助手已就位。您以【审核人·李主管】身份进入 CUSA-Ultra 项目私聊工作区;此处仅您与智能体可见,编制人/批准人对话不会出现在本窗口。',
+ timestamp: '16:14',
+ },
+ {
+ id: 'm-cusa-li-1',
+ sessionId: buildPrivateSessionId('cusa', 2),
+ role: 'assistant',
+ authorName: '项目助手',
+ intentTag: '🎯 审核人私聊就绪',
+ content: '李主管您好!本窗口为您的专属项目助手私聊。您可在此进行法规合规咨询、添加审查意见,或下达审核/驳回指令。项目事实黑板仍跨角色共享,但对话记录严格隔离。',
+ timestamp: '16:14',
+ },
+ {
+ id: 'm-cusa-8',
+ sessionId: buildPrivateSessionId('cusa', 2),
+ role: 'user',
+ authorName: '李主管',
+ content:
+ '从法规与临床评价角度看,该超声刀头属于接触患者深层中枢神经的耗材,在同品种比对和临床试验路径上有什么指导原则可以参考?',
+ timestamp: '16:15',
+ },
+ {
+ id: 'm-cusa-9',
+ sessionId: buildPrivateSessionId('cusa', 2),
+ role: 'assistant',
+ authorName: '项目助手',
+ intentTag: '⚖️ 审核人合规与临床评价咨询',
+ content:
+ '李主管您好!针对 CUSA 超声吸引系统的同品种比对路径研判如下:\n\n1. **首选同品种比对(CER)路径**:\n - **主对照器械**:建议选取 **Integra LifeSciences CUSA® Excel / CUSA® Clarity**(已持有有效 NMPA 进口三类注册证);\n - **技术等同性**:两者的工作机制(空化剥离)、工作频率(24kHz/55kHz)、刀头接触材质(TC4 ELI)及输出能量控制范围完全一致;\n - **差异性支持论据**:我方的频率跟踪锁相环(PLL)响应速度更快,可通过动物离体肝脏与脑组织对比试验(组织热损伤切片对比、乳化残留率对比)证明其安全有效性与对照品等同,有望免于开展大规模人体临床试验。\n2. **指导原则依据**:参考 NMPA《医疗器械临床评价技术指导原则》及《有源手术器械临床评价指导原则》。',
+ timestamp: '16:17',
+ },
+ {
+ id: 'm-cusa-10',
+ sessionId: buildPrivateSessionId('cusa', 2),
+ role: 'user',
+ authorName: '李主管',
+ content:
+ '顺便请教一个学术问题,在空化效应中,Rayleigh-Plesset 气泡动力学方程的核心物理机制是什么?',
+ timestamp: '16:20',
+ },
+ {
+ id: 'm-cusa-11',
+ sessionId: buildPrivateSessionId('cusa', 2),
+ role: 'assistant',
+ authorName: '项目助手',
+ intentTag: '🔬 流体力学物理方程学术推演',
+ content:
+ '非常经典的流体力学非线性动力学问题!Rayleigh-Plesset 方程描述了在无限均匀液体介质中受高频外场驱动的单空化微气泡半径 $R(t)$ 随时间的演化行为:\n\n$$\nR\\ddot{R} + \\frac{3}{2}\\dot{R}^2 = \\frac{1}{\\rho}\\left[ p_B(t) - p_\\infty(t) - \\frac{2\\gamma}{R} - \\frac{4\\mu\\dot{R}}{R} \\right]\n$$\n\n核心物理机制拆解:\n1. **左侧惯性项**:$R\\ddot{R} + \\frac{3}{2}\\dot{R}^2$ 表示液体随气泡壁急剧加/减速运动的对流惯性动能。\n2. **右侧驱动与阻力项**:\n - $p_B(t) - p_\\infty(t)$:气泡内部气体/蒸汽压与外部超声驱动声场的净压差;\n - $-\\frac{2\\gamma}{R}$:液体表面张力引起的 Laplace 收缩压;\n - $-\\frac{4\\mu\\dot{R}}{R}$:液体粘性剪切耗散阻尼。\n\n在 24kHz 声场负压相时,气泡膨胀至临界半径 $R_{\\max}$;在正压相时,外部巨大水锤压力驱动气泡剧烈坍缩崩溃(倒塌速度接近声速),最终释放局部数千度的瞬态高温和极速微射流(Micro-jet),这正是精准粉碎肿瘤细胞的微观物理根源!',
+ timestamp: '16:22',
+ },
+ {
+ id: 'm-cusa-12',
+ sessionId: buildPrivateSessionId('cusa', 2),
+ role: 'user',
+ authorName: '李主管',
+ content:
+ '补充合规标准要求:必须符合 GB 9706.202-2021 高频手术设备、YY 0505 EMC、GB/T 16886.1 生物相容性五项检测,且 EO 残存量须小于 5μg/g。',
+ timestamp: '16:25',
+ },
+ {
+ id: 'm-cusa-13',
+ sessionId: buildPrivateSessionId('cusa', 2),
+ role: 'assistant',
+ authorName: '项目助手',
+ intentTag: '🛡️ 审核人补充法规强标约束',
+ content:
+ '已将技术主管【李主管】下达的法定合规标准与灭菌残存阈值存入项目约束基线!\n- `standards`: GB 9706.1-2020 / GB 9706.202-2021 / YY 0505-2012\n- `biocompatibility`: GB/T 16886.1-2022 (五项必检:细胞毒性、致敏、皮内刺激、急性全身毒性、热原)\n- `sterilization`: EO 灭菌残留 $\\le 5\\,\\mu\\text{g/g}$\n\n该批标准已锁定为 V0.5 阶段门禁核验硬性前置条件。',
+ timestamp: '16:26',
+ },
+ {
+ id: 'm-cusa-14',
+ sessionId: buildPrivateSessionId('cusa', 1),
+ role: 'user',
+ authorName: '张工',
+ content:
+ '请项目助手启动自由专项调研任务:针对 24kHz 高频微幅振动下的钛合金刀头微裂纹与疲劳剥落风险进行深度专项推演,并形成分析报告。',
+ timestamp: '16:30',
+ },
+ {
+ id: 'm-cusa-15',
+ sessionId: buildPrivateSessionId('cusa', 1),
+ role: 'assistant',
+ authorName: '项目助手',
+ intentTag: '⚡ 自由专项推演任务完成',
+ content:
+ '【自由专项任务】《24kHz高频空化钛合金刀头微裂纹与疲劳剥落风险专项分析报告》已利用真实 Agent 引擎推演完成!\n\n核心推演结论:\n1. **高周疲劳风险点**:刀头细颈变幅截面在 $10^7$ 次循环下拉应力集中,推荐采用微细丸喷丸引入 200~300 MPa 残余压应力层;\n2. **耐空化冲击硬化**:表面采用微弧氧化生成金红石相 $\\text{TiO}_2$ 致密防护膜;\n3. **阻抗主动防御**:主机植入谐振频率偏移实时监测(频移 $>300\\text{ Hz}$ 自动停机防断刀)。\n\n报告已归档至项目共享资料库,您可随时在右侧查阅。',
+ timestamp: '16:32',
+ phaseEvents: [
+ { step: '1', icon: '🔍', label: '应力建模', status: 'completed' },
+ { step: '2', icon: '💥', label: '空化冲击分析', status: 'completed' },
+ { step: '3', icon: '🛡️', label: '硬化对策', status: 'completed' },
+ { step: '4', icon: '📄', label: '报告产出', status: 'completed' },
+ ],
+ },
+ {
+ id: 'm-cusa-16',
+ sessionId: buildPrivateSessionId('cusa', 1),
+ role: 'user',
+ authorName: '张工',
+ content:
+ '《立项与可行性法规论证报告》编制完成,请求提交李主管审核!',
+ timestamp: '16:35',
+ },
+ {
+ id: 'm-cusa-17',
+ sessionId: buildPrivateSessionId('cusa', 1),
+ role: 'assistant',
+ authorName: '项目助手',
+ intentTag: '🔒 文档锁定并提请合规审核',
+ content:
+ '已受理编制人【张工】的送审请求!\n\n受控文档《数字化高频超声手术吸引系统立项与可行性法规论证报告》已自动锁定正文,生命周期进入【审核中 (reviewing)】状态。已生成审核任务并呈送李主管。',
+ timestamp: '16:36',
+ },
+ {
+ id: 'm-cusa-18',
+ sessionId: buildPrivateSessionId('cusa', 2),
+ role: 'user',
+ authorName: '李主管',
+ content:
+ '经核验,钛合金生物相容性及电磁兼容方案完备,审核通过,呈报王总监批准!',
+ timestamp: '16:40',
+ },
+ {
+ id: 'm-cusa-19',
+ sessionId: buildPrivateSessionId('cusa', 2),
+ role: 'assistant',
+ authorName: '项目助手',
+ intentTag: '📋 审核通过流转至终审',
+ content:
+ '审核负责人【李主管】已完成专业审核,结论:【审核通过】!\n\n受控文档状态已流转至【待批准 (approving)】,已呈报微创器械研发总监【王总监】进行最终批准决策。',
+ timestamp: '16:41',
+ },
+
+ // --- 王总监私聊会话开场 ---
+ {
+ id: 'm-cusa-wang-0',
+ sessionId: buildPrivateSessionId('cusa', 3),
+ role: 'system',
+ content: '项目助手已就位。您以【批准人·王总监】身份进入 CUSA-Ultra 项目私聊工作区;此处仅您与智能体可见。',
+ timestamp: '16:44',
+ },
+ {
+ id: 'm-cusa-wang-1',
+ sessionId: buildPrivateSessionId('cusa', 3),
+ role: 'assistant',
+ authorName: '项目助手',
+ intentTag: '🎯 批准人私聊就绪',
+ content: '王总监您好!本窗口为您的专属终审私聊。您可在此查阅项目助手摘要、确认批准或驳回决策。编制人与审核人的对话内容不会出现在本窗口。',
+ timestamp: '16:44',
+ },
+ {
+ id: 'm-cusa-20',
+ sessionId: buildPrivateSessionId('cusa', 3),
+ role: 'user',
+ authorName: '王总监',
+ content:
+ '技术路线扎实,合规路径清晰,同意立项并准予发布!',
+ timestamp: '16:45',
+ },
+ {
+ id: 'm-cusa-21',
+ sessionId: buildPrivateSessionId('cusa', 3),
+ role: 'assistant',
+ authorName: '项目助手',
+ intentTag: '🎉 项目立项阶段终审批准发布',
+ content:
+ '微创器械研发总监【王总监】已完成最终批准决策!\n\n《数字化高频超声手术吸引系统立项与可行性法规论证报告》正式流转至【已批准 (approved)】正式受控版本!全生命周期数字水印已盖印,关键技术特征已自动切片并向量化归档至水木东方共享合规案例库。恭喜研发小队顺利达成 V0.5 阶段性里程碑!',
+ timestamp: '16:46',
+ phaseEvents: [
+ { step: '1', icon: '✍️', label: '编制完成', status: 'completed' },
+ { step: '2', icon: '🔍', label: '主管审核', status: 'completed' },
+ { step: '3', icon: '👑', label: '总监批准', status: 'completed' },
+ { step: '4', icon: '📚', label: '知识库沉淀', status: 'completed' },
+ ],
+ },
+
+ // 张工私聊:4K超高清荧光内窥镜 (sess-endo-u1)
{
id: 'm-001-0',
- sessionId: 'sess-001',
+ sessionId: buildPrivateSessionId('endo', 1),
role: 'system',
- content: '水木东方协同参谋小队已就位,当前项目进入【V0.5 方案成型与法规对齐】阶段。',
+ content: '项目助手已就位,当前项目进入【V0.5 方案成型与法规对齐】阶段。',
timestamp: '14:00',
},
{
id: 'm-001-1',
- sessionId: 'sess-001',
+ sessionId: buildPrivateSessionId('endo', 1),
role: 'assistant',
- authorName: '水木东方协同参谋',
+ authorName: '项目助手',
intentTag: '🎯 推进立项阶段',
content:
- '张工您好!参谋小队已依据法规知识库完成《4K超高清荧光内窥镜系统立项与可行性报告》初稿编制。右侧已就绪块状结构化视图,您可以直接审阅或下达修改指令。',
+ '张工您好!项目助手已依据法规知识库完成《4K超高清荧光内窥镜系统立项与可行性报告》初稿编制。右侧已就绪块状结构化视图,您可以直接审阅或下达修改指令。',
timestamp: '14:02',
phaseEvents: [
{ step: '1', icon: '👥', label: '组装小队', status: 'completed' },
@@ -81,19 +402,19 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
],
},
- // sess-002 专属对话 (便携式掌上无线超声诊断仪)
+ // 张工私聊:便携式掌上无线超声诊断仪 (sess-ultrasound-u1)
{
id: 'm-002-0',
- sessionId: 'sess-002',
+ sessionId: buildPrivateSessionId('ultrasound', 1),
role: 'system',
- content: '水木东方协同参谋小队已就位,当前项目进入【V0.1 需求摸底与产品定义】阶段。',
+ content: '项目助手已就位,当前项目进入【V0.1 需求摸底与产品定义】阶段。',
timestamp: '09:30',
},
{
id: 'm-002-1',
- sessionId: 'sess-002',
+ sessionId: buildPrivateSessionId('ultrasound', 1),
role: 'assistant',
- authorName: '水木东方协同参谋',
+ authorName: '项目助手',
intentTag: '💡 掌超项目摸底',
content:
'张工,便携式掌上无线超声诊断仪立项需求摸底已启动!\n\n小队已针对其换能器阵元数 (128阵元)、无线 Wi-Fi 6 低延迟图传以及 GB 9706.237-2020 超声声输出安全标准进行技术基线梳理。您可以直接提出需求调整或测算指令。',
@@ -131,14 +452,31 @@ function safeSetItem(key: string, val: T): void {
}
export function loadSessions(): ProjectSession[] {
- return safeGetItem(STORAGE_KEYS.SESSIONS, DEFAULT_SESSIONS);
+ const loaded = safeGetItem(STORAGE_KEYS.SESSIONS, DEFAULT_SESSIONS);
+ // 丢弃无 userId 的旧混聊会话,强制走每人独立私聊模型
+ const sanitized = loaded.filter((s) => typeof s.userId === 'number' && s.userId > 0);
+ const base = sanitized.length > 0 ? sanitized : DEFAULT_SESSIONS;
+ const existingIds = new Set(base.map((s) => s.sessionId));
+ const missing = DEFAULT_SESSIONS.filter((s) => !existingIds.has(s.sessionId));
+ const merged = missing.length > 0 ? [...missing, ...base] : base;
+
+ // 用默认消息数回填 messageCount,便于选中「有历史」的私聊而非空壳
+ const counts: Record = {};
+ for (const m of DEFAULT_MESSAGES) {
+ counts[m.sessionId] = (counts[m.sessionId] || 0) + 1;
+ }
+ const withCounts = merged.map((s) => ({
+ ...s,
+ messageCount: Math.max(s.messageCount || 0, counts[s.sessionId] || 0),
+ }));
+ return dedupePrivateSessions(withCounts, counts);
}
export function saveSessions(sessions: ProjectSession[]): void {
safeSetItem(STORAGE_KEYS.SESSIONS, sessions);
}
-export function loadActiveSessionId(fallback = 'sess-001'): string {
+export function loadActiveSessionId(fallback = NEUROPULSE_ACTIVE_SESSION_ID): string {
if (typeof window === 'undefined' || !window.localStorage) return fallback;
return window.localStorage.getItem(STORAGE_KEYS.ACTIVE_SESSION) || fallback;
}
@@ -149,7 +487,20 @@ export function saveActiveSessionId(sessionId: string): void {
}
export function loadMessages(): ChatMessage[] {
- return safeGetItem(STORAGE_KEYS.MESSAGES, DEFAULT_MESSAGES);
+ const loaded = safeGetItem(STORAGE_KEYS.MESSAGES, DEFAULT_MESSAGES);
+ // 丢弃旧版混聊 sessionId,避免「有消息但当前私聊窗口对不上」
+ const legacyIds = new Set(['sess-cusa-001', 'sess-001', 'sess-002']);
+ const cleaned = loaded.filter((m) => !legacyIds.has(m.sessionId));
+ const existingIds = new Set(cleaned.map((m) => m.id));
+ const missing = DEFAULT_MESSAGES.filter((m) => !existingIds.has(m.id));
+ if (missing.length > 0) {
+ return [...cleaned, ...missing];
+ }
+ // 若清洗后几乎为空,回退默认预设
+ if (cleaned.length === 0) {
+ return [...DEFAULT_MESSAGES];
+ }
+ return cleaned;
}
export function saveMessages(messages: ChatMessage[]): void {
@@ -157,7 +508,13 @@ export function saveMessages(messages: ChatMessage[]): void {
}
export function loadDeliverables(fallback: DeliverableItem[]): DeliverableItem[] {
- return safeGetItem(STORAGE_KEYS.DELIVERABLES, fallback);
+ const loaded = safeGetItem(STORAGE_KEYS.DELIVERABLES, fallback);
+ const existingDocIds = new Set(loaded.map((d) => d.docId));
+ const missing = fallback.filter((d) => !existingDocIds.has(d.docId));
+ if (missing.length > 0) {
+ return [...loaded, ...missing];
+ }
+ return loaded;
}
export function saveDeliverables(deliverables: DeliverableItem[]): void {
diff --git a/web/src/engine/cardActionEngine.test.ts b/web/src/engine/cardActionEngine.test.ts
index 902c3ab8..d29e20d0 100644
--- a/web/src/engine/cardActionEngine.test.ts
+++ b/web/src/engine/cardActionEngine.test.ts
@@ -90,9 +90,11 @@ test('createRejectConfirmationCard: 生成合规危险级驳回门禁卡片', ()
const confirmAction = card.actions.find((a) => a.actionId === 'confirm_reject');
assert.ok(confirmAction);
- assert.equal(confirmAction.style, 'danger');
- assert.equal(confirmAction.isAffirmativeDefault, true);
- assert.equal(confirmAction.payload?.docId, 'doc-002');
+ assert.equal(confirmAction!.label, '确认驳回退回编制');
+ assert.equal(confirmAction!.style, 'danger');
+ assert.equal(confirmAction!.isAffirmativeDefault, true);
+ assert.equal(confirmAction!.payload?.docId, 'doc-002');
+ assert.equal(card.actions.find((a) => a.actionId === 'cancel')?.label, '放弃驳回');
});
test('isAffirmativeResponse: 准确识别双轨交互中的肯定意图输入', () => {
diff --git a/web/src/engine/cardActionEngine.ts b/web/src/engine/cardActionEngine.ts
index 63aa98d7..9f630354 100644
--- a/web/src/engine/cardActionEngine.ts
+++ b/web/src/engine/cardActionEngine.ts
@@ -43,26 +43,31 @@ export function findMatchingBlockForReview(ast: DocumentAST, hint?: string): Doc
}
/**
- * 构建审核驳回流转确认交互卡片
+ * 构建审核驳回流转确认交互卡片(仅供审核/批准执行路径;编制人查询请用 createRejectReasonInfoCard)
*/
export function createRejectConfirmationCard(
targetDoc: { docId: string; title: string },
- matchingBlock?: DocumentBlock
+ matchingBlock?: DocumentBlock,
+ options?: { reason?: string; mode?: 'review' | 'final' }
): InteractiveCardData {
const cardId = 'card-reject-' + Date.now();
const sectionLabel = matchingBlock?.numbering
? `${matchingBlock.numbering} ${matchingBlock.content.slice(0, 24)}`
: '第3节「电气安全与合规风险对标」';
+ const mode = options?.mode || 'review';
+ const reason =
+ options?.reason ||
+ '【驳回审查意见】关键技术指标与竞品对比不充分,需补充电气安全与EMC合规佐证,退回编制人重新修订。';
return {
cardId,
cardType: 'CONFIRMATION',
- title: '审核驳回与返工确认',
+ title: mode === 'final' ? '终审驳回确认' : '审核驳回与返工确认',
targetDocId: targetDoc.docId,
targetDocTitle: targetDoc.title,
targetSectionId: matchingBlock?.id || 'sec-3',
sectionTitle: sectionLabel,
- proposedContent: '【驳回审查意见】关键技术指标与竞品对比不充分,需补充电气安全与EMC合规佐证,退回编制人重新修订。',
+ proposedContent: reason,
impactRisk: '高风险:将退回至草稿编制阶段,正文锁定将解除',
status: 'active',
ttlTurns: 3,
@@ -70,18 +75,19 @@ export function createRejectConfirmationCard(
actions: [
{
actionId: 'confirm_reject',
- label: '确认驳回并退回草稿',
+ label: '确认驳回退回编制',
style: 'danger',
isAffirmativeDefault: true,
payload: {
docId: targetDoc.docId,
- reason: '关键竞品性能指标与电气安全指标待补充,退回编制人重新修订。',
+ reason,
targetSection: sectionLabel,
+ target_action: mode === 'final' ? 'confirm_final_reject' : 'confirm_reject',
},
},
{
actionId: 'cancel',
- label: '放弃操作',
+ label: '放弃驳回',
style: 'subtle',
payload: {
docId: targetDoc.docId,
@@ -91,6 +97,69 @@ export function createRejectConfirmationCard(
};
}
+/**
+ * 构建驳回原因说明卡(编制人/查询路径;无危险流转按钮)
+ */
+export function createRejectReasonInfoCard(
+ targetDoc: { docId: string; title: string },
+ reason: string,
+ opts?: { includeStartRevision?: boolean; sectionLabel?: string; sectionId?: string }
+): InteractiveCardData {
+ const actions: InteractiveCardData['actions'] = [
+ {
+ actionId: 'ack_reject_reason',
+ label: '知道了',
+ style: 'primary',
+ isAffirmativeDefault: true,
+ payload: { docId: targetDoc.docId },
+ },
+ ];
+ if (opts?.includeStartRevision !== false) {
+ actions.push({
+ actionId: 'start_revision',
+ label: '开始按意见修订',
+ style: 'subtle',
+ payload: { docId: targetDoc.docId },
+ });
+ }
+ return {
+ cardId: 'card-reject-reason-' + Date.now(),
+ cardType: 'REMINDER_NOTICE',
+ title: '驳回原因说明',
+ targetDocId: targetDoc.docId,
+ targetDocTitle: targetDoc.title,
+ targetSectionId: opts?.sectionId,
+ sectionTitle: opts?.sectionLabel,
+ proposedContent: reason,
+ impactRisk: '本卡片为查询说明,不会变更文档生命周期状态',
+ status: 'active',
+ ttlTurns: 5,
+ createdAt: new Date().toTimeString().substring(0, 5),
+ actions,
+ };
+}
+
+/**
+ * 从文档 AST 中提取最近一条 issue 类驳回批注
+ */
+export function extractLatestRejectReasonFromAst(ast: DocumentAST): string | undefined {
+ const collect = (block: DocumentBlock | undefined): string[] => {
+ if (!block) return [];
+ const out: string[] = [];
+ for (const c of block.comments || []) {
+ if (c.type === 'issue' || (c.content && /驳回|退回|缺少|需补充/.test(c.content))) {
+ out.push(c.content);
+ }
+ }
+ for (const child of block.children || []) {
+ out.push(...collect(child));
+ }
+ return out;
+ };
+ const reasons = collect(ast.rootBlock);
+ return reasons.length > 0 ? reasons[reasons.length - 1] : undefined;
+}
+
/**
* 判定用户输入是否属于肯定型双轨回复(好、确认、同意等)
*/
diff --git a/web/src/engine/cardActionPolicy.test.ts b/web/src/engine/cardActionPolicy.test.ts
new file mode 100644
index 00000000..b176c77e
--- /dev/null
+++ b/web/src/engine/cardActionPolicy.test.ts
@@ -0,0 +1,113 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import {
+ classifyRejectUtterance,
+ resolveDocGovCard,
+ canConfirmReject,
+} from './cardActionPolicy';
+
+test('classifyRejectUtterance: 询问原因 → QUERY_REJECT_REASON', () => {
+ assert.equal(
+ classifyRejectUtterance('注册路径可行性评估报告被驳回的原因是什么'),
+ 'QUERY_REJECT_REASON'
+ );
+ assert.equal(classifyRejectUtterance('为什么被退回了'), 'QUERY_REJECT_REASON');
+ assert.equal(classifyRejectUtterance('驳回意见是什么'), 'QUERY_REJECT_REASON');
+});
+
+test('classifyRejectUtterance: 明确执行 → EXECUTE_REJECT', () => {
+ assert.equal(
+ classifyRejectUtterance('把调研报告审核驳回,意见为:缺少抗扰度指标'),
+ 'EXECUTE_REJECT'
+ );
+ assert.equal(classifyRejectUtterance('请驳回这份文档'), 'EXECUTE_REJECT');
+ assert.equal(classifyRejectUtterance('终审驳回,意见:资料不全'), 'EXECUTE_REJECT');
+});
+
+test('classifyRejectUtterance: 模糊 → OTHER', () => {
+ assert.equal(classifyRejectUtterance('驳回了吗'), 'OTHER');
+ assert.equal(classifyRejectUtterance('今天天气不错'), 'OTHER');
+});
+
+test('resolveDocGovCard: 编制人查询 → 信息卡且无 confirm_reject', () => {
+ const res = resolveDocGovCard({
+ role: 'author',
+ lifecycleState: 'drafting',
+ utteranceIntent: 'QUERY_REJECT_REASON',
+ docId: 'doc-feas',
+ docTitle: '注册路径可行性评估报告',
+ latestRejectReason: '缺少 YY 9706.102 电磁兼容专项与听神经保护方案,退回修改',
+ });
+ assert.equal(res.kind, 'reject_reason_info');
+ assert.ok(res.card);
+ assert.equal(res.card!.cardType, 'REMINDER_NOTICE');
+ assert.ok(!res.card!.actions.some((a) => a.actionId === 'confirm_reject'));
+ assert.ok(res.card!.actions.some((a) => a.actionId === 'ack_reject_reason'));
+ assert.ok(res.card!.actions.some((a) => a.actionId === 'start_revision'));
+ assert.ok(res.replyContent.includes('YY 9706.102'));
+});
+
+test('resolveDocGovCard: 审核人执行 + reviewing → 确认卡', () => {
+ const res = resolveDocGovCard({
+ role: 'reviewer',
+ lifecycleState: 'reviewing',
+ utteranceIntent: 'EXECUTE_REJECT',
+ docId: 'doc-002',
+ docTitle: '竞品调研',
+ latestRejectReason: '缺少抗扰度指标',
+ });
+ assert.equal(res.kind, 'reject_confirm_review');
+ assert.ok(res.card);
+ assert.equal(res.card!.cardType, 'CONFIRMATION');
+ const confirm = res.card!.actions.find((a) => a.actionId === 'confirm_reject');
+ assert.ok(confirm);
+ assert.equal(confirm!.label, '确认驳回退回编制');
+ assert.equal(res.card!.actions.find((a) => a.actionId === 'cancel')?.label, '放弃驳回');
+});
+
+test('resolveDocGovCard: 编制人误说执行驳回 → 无权纯文本', () => {
+ const res = resolveDocGovCard({
+ role: 'author',
+ lifecycleState: 'reviewing',
+ utteranceIntent: 'EXECUTE_REJECT',
+ docId: 'doc-feas',
+ docTitle: '可行性报告',
+ });
+ assert.equal(res.kind, 'deny_text');
+ assert.equal(res.card, undefined);
+ assert.ok(res.replyContent.includes('不具备'));
+});
+
+test('resolveDocGovCard: 批准人 + approving → 终审确认卡', () => {
+ const res = resolveDocGovCard({
+ role: 'approver',
+ lifecycleState: 'approving',
+ utteranceIntent: 'EXECUTE_REJECT',
+ docId: 'doc-003',
+ docTitle: '终审稿',
+ latestRejectReason: '临床证据不足',
+ });
+ assert.equal(res.kind, 'reject_confirm_final');
+ assert.equal(res.card?.title, '终审驳回确认');
+});
+
+test('resolveDocGovCard: 编制人 OTHER → 引导查询无危险按钮', () => {
+ const res = resolveDocGovCard({
+ role: 'author',
+ lifecycleState: 'drafting',
+ utteranceIntent: 'OTHER',
+ docId: 'doc-feas',
+ docTitle: '可行性报告',
+ latestRejectReason: '缺 EMC',
+ });
+ assert.equal(res.kind, 'deny_text');
+ assert.equal(res.card, undefined);
+ assert.ok(res.replyContent.includes('编制'));
+});
+
+test('canConfirmReject: 仅审核/批准', () => {
+ assert.equal(canConfirmReject('author'), false);
+ assert.equal(canConfirmReject('viewer'), false);
+ assert.equal(canConfirmReject('reviewer'), true);
+ assert.equal(canConfirmReject('approver'), true);
+});
diff --git a/web/src/engine/cardActionPolicy.ts b/web/src/engine/cardActionPolicy.ts
new file mode 100644
index 00000000..ae6c5962
--- /dev/null
+++ b/web/src/engine/cardActionPolicy.ts
@@ -0,0 +1,318 @@
+import { DocLifecycleState, DocPermissionRole } from '../types/docgov';
+import { CardActionItem, InteractiveCardData } from '../types/workbench';
+
+/** 驳回相关话语意图 */
+export type RejectUtteranceIntent = 'QUERY_REJECT_REASON' | 'EXECUTE_REJECT' | 'OTHER';
+
+export type DocGovCardKind =
+ | 'reject_confirm_review'
+ | 'reject_confirm_final'
+ | 'reject_reason_info'
+ | 'deny_text';
+
+export interface ResolveDocGovCardInput {
+ role: DocPermissionRole | string;
+ lifecycleState: DocLifecycleState | string;
+ utteranceIntent: RejectUtteranceIntent;
+ docId: string;
+ docTitle: string;
+ latestRejectReason?: string;
+ sectionLabel?: string;
+ sectionId?: string;
+}
+
+export interface ResolveDocGovCardResult {
+ kind: DocGovCardKind;
+ /** 助手气泡正文 */
+ replyContent: string;
+ intentTag: string;
+ /** 可挂载的交互卡;deny_text 时为 undefined */
+ card?: InteractiveCardData;
+ denyReason?: string;
+}
+
+const QUERY_MARKERS = [
+ '原因',
+ '为何',
+ '为什么',
+ '怎么回事',
+ '什么情况',
+ '什么意见',
+ '哪点',
+ '哪些问题',
+ '整改',
+ '意见是什么',
+ '理由',
+ '怎么改',
+ '如何修改',
+ '查一下驳回',
+ '看看驳回',
+];
+
+const EXECUTE_MARKERS = [
+ '请驳回',
+ '帮我驳回',
+ '把它驳回',
+ '把这份',
+ '把该',
+ '审核驳回',
+ '批准驳回',
+ '终审驳回',
+ '正式驳回',
+ '执行驳回',
+ '确认驳回',
+ '驳回这份',
+ '驳回该',
+ '驳回报告',
+ '驳回文档',
+ '予以驳回',
+ '决定驳回',
+ '我要驳回',
+ '我要退回',
+ '退回编制',
+ '退回修改',
+];
+
+/**
+ * 区分「询问驳回原因」与「发起驳回执行」。
+ * 不含驳回/退回关键词时返回 OTHER。
+ * 明确执行句优先于查询句(避免「驳回,意见为:…」被误判为查询)。
+ */
+export function classifyRejectUtterance(text: string): RejectUtteranceIntent {
+ const t = text.trim();
+ if (!t) return 'OTHER';
+ if (!t.includes('驳回') && !t.includes('退回')) return 'OTHER';
+
+ const isExecute =
+ EXECUTE_MARKERS.some((m) => t.includes(m)) ||
+ /(?:把|将).*(?:驳回|退回)/.test(t) ||
+ /驳回[,,::].+/.test(t) ||
+ /意见为|意见:|意见:/.test(t);
+
+ if (isExecute) return 'EXECUTE_REJECT';
+
+ const isQuery =
+ QUERY_MARKERS.some((m) => t.includes(m)) ||
+ /被驳回/.test(t) ||
+ /为何.*(?:驳回|退回)|为什么.*(?:驳回|退回)|(?:驳回|退回).*(?:原因|理由|意见是什么|什么意见)/.test(t);
+
+ if (isQuery) return 'QUERY_REJECT_REASON';
+
+ // 含驳回但语义不清:交给角色策略(非审核/批准不出危险卡)
+ return 'OTHER';
+}
+
+function roleLabel(role: string): string {
+ switch (role) {
+ case 'author':
+ return '编制负责人';
+ case 'reviewer':
+ return '审核负责人';
+ case 'approver':
+ return '批准负责人';
+ case 'viewer':
+ return '查阅者';
+ default:
+ return role || '未指定';
+ }
+}
+
+function defaultRejectReason(reason?: string): string {
+ const r = (reason || '').trim();
+ return r || '(暂未检索到结构化驳回批注,请打开文档批注面板或联系审核人确认。)';
+}
+
+function buildInfoCard(input: ResolveDocGovCardInput, reason: string): InteractiveCardData {
+ const actions: CardActionItem[] = [
+ {
+ actionId: 'ack_reject_reason',
+ label: '知道了',
+ style: 'primary',
+ isAffirmativeDefault: true,
+ payload: { docId: input.docId },
+ },
+ ];
+ if (input.role === 'author') {
+ actions.push({
+ actionId: 'start_revision',
+ label: '开始按意见修订',
+ style: 'subtle',
+ payload: { docId: input.docId },
+ });
+ }
+
+ return {
+ cardId: 'card-reject-reason-' + Date.now(),
+ cardType: 'REMINDER_NOTICE',
+ title: '驳回原因说明',
+ targetDocId: input.docId,
+ targetDocTitle: input.docTitle,
+ targetSectionId: input.sectionId,
+ sectionTitle: input.sectionLabel,
+ proposedContent: reason,
+ impactRisk: '本卡片为查询说明,不会变更文档生命周期状态',
+ status: 'active',
+ ttlTurns: 5,
+ createdAt: new Date().toTimeString().substring(0, 5),
+ actions,
+ };
+}
+
+function buildRejectConfirmCard(
+ input: ResolveDocGovCardInput,
+ mode: 'review' | 'final',
+ reason: string
+): InteractiveCardData {
+ const title = mode === 'final' ? '终审驳回确认' : '审核驳回与返工确认';
+ const sectionLabel = input.sectionLabel || '拟注入审查批注位置';
+ return {
+ cardId: 'card-reject-' + Date.now(),
+ cardType: 'CONFIRMATION',
+ title,
+ targetDocId: input.docId,
+ targetDocTitle: input.docTitle,
+ targetSectionId: input.sectionId,
+ sectionTitle: sectionLabel,
+ proposedContent: reason,
+ impactRisk:
+ mode === 'final'
+ ? '高风险:终审驳回将退回编制阶段,解除终审锁定并向编制人下达整改批注'
+ : '高风险:将退回至草稿编制阶段,正文锁定将解除',
+ status: 'active',
+ ttlTurns: 3,
+ createdAt: new Date().toTimeString().substring(0, 5),
+ actions: [
+ {
+ actionId: 'confirm_reject',
+ label: '确认驳回退回编制',
+ style: 'danger',
+ isAffirmativeDefault: true,
+ payload: {
+ docId: input.docId,
+ reason,
+ targetSection: sectionLabel,
+ target_action: mode === 'final' ? 'confirm_final_reject' : 'confirm_reject',
+ },
+ },
+ {
+ actionId: 'cancel',
+ label: '放弃驳回',
+ style: 'subtle',
+ payload: { docId: input.docId },
+ },
+ ],
+ };
+}
+
+/**
+ * 按角色 × 文档状态 × 话语意图解析应下发的卡片或拒绝说明。
+ */
+export function resolveDocGovCard(input: ResolveDocGovCardInput): ResolveDocGovCardResult {
+ const role = (input.role || '').toLowerCase();
+ const state = (input.lifecycleState || '').toLowerCase();
+ const reason = defaultRejectReason(input.latestRejectReason);
+ const title = input.docTitle || '受控文档';
+
+ if (input.utteranceIntent === 'QUERY_REJECT_REASON') {
+ if (role === 'viewer') {
+ return {
+ kind: 'deny_text',
+ intentTag: '📋 驳回原因查阅',
+ replyContent: `关于《${title}》的驳回情况:${reason}\n\n您当前为查阅者,可查看说明但无法执行编制修订或审签流转。`,
+ denyReason: 'VIEWER_QUERY_ONLY',
+ };
+ }
+ const card = buildInfoCard(input, reason);
+ return {
+ kind: 'reject_reason_info',
+ intentTag: '📋 驳回原因说明',
+ replyContent: `关于《${title}》的驳回说明如下。本说明不会变更文档状态:\n\n${reason}`,
+ card,
+ };
+ }
+
+ if (input.utteranceIntent === 'EXECUTE_REJECT') {
+ if (role === 'reviewer' && state === 'reviewing') {
+ const card = buildRejectConfirmCard(input, 'review', reason);
+ return {
+ kind: 'reject_confirm_review',
+ intentTag: '🛡️ 审核驳回门禁',
+ replyContent: `检测到您对《${title}》提出审核驳回意图。驳回将退回编制阶段并解除锁定,同时向编制人下达整改批注。请确认操作:`,
+ card,
+ };
+ }
+ if (role === 'approver' && state === 'approving') {
+ const card = buildRejectConfirmCard(input, 'final', reason);
+ return {
+ kind: 'reject_confirm_final',
+ intentTag: '🛡️ 终审驳回门禁',
+ replyContent: `检测到您对《${title}》提出终审驳回意图。驳回将退回编制阶段并向编制人下达整改批注。请确认操作:`,
+ card,
+ };
+ }
+
+ // 角色或状态不符
+ if (role === 'author' || role === 'viewer') {
+ return {
+ kind: 'deny_text',
+ intentTag: '⚠️ 权限不符',
+ replyContent: `您当前在《${title}》中的身份为「${roleLabel(role)}」,不具备审核/终审驳回权限。若需了解驳回原因,可直接询问「被驳回的原因是什么」。`,
+ denyReason: 'UNAUTHORIZED_REJECT_ROLE',
+ };
+ }
+ if (role === 'reviewer' && state !== 'reviewing') {
+ return {
+ kind: 'deny_text',
+ intentTag: '⚠️ 状态校验提示',
+ replyContent: `当前受控文档《${title}》处于【${state || '未知'}】阶段,非待审核(reviewing)状态,无法执行审核驳回。`,
+ denyReason: 'INVALID_STATE_FOR_REVIEW_REJECT',
+ };
+ }
+ if (role === 'approver' && state !== 'approving') {
+ return {
+ kind: 'deny_text',
+ intentTag: '⚠️ 状态校验提示',
+ replyContent: `当前受控文档《${title}》处于【${state || '未知'}】阶段,非终审(approving)状态,无法执行终审驳回。`,
+ denyReason: 'INVALID_STATE_FOR_FINAL_REJECT',
+ };
+ }
+ return {
+ kind: 'deny_text',
+ intentTag: '⚠️ 权限不符',
+ replyContent: `无法为当前身份「${roleLabel(role)}」在状态【${state || '未知'}】下执行驳回。`,
+ denyReason: 'UNAUTHORIZED_OR_INVALID_STATE',
+ };
+ }
+
+ // OTHER:非审核/批准角色不出确认卡;审核/批准在合法状态可提示澄清
+ if (role === 'reviewer' && state === 'reviewing') {
+ return {
+ kind: 'deny_text',
+ intentTag: '💬 请明确意图',
+ replyContent: `您提到了「驳回」。若要执行审核驳回,请明确说明驳回意见(例如「把《${title}》审核驳回,意见为:…」);若只是查询原因,请直接询问「被驳回的原因是什么」。`,
+ denyReason: 'AMBIGUOUS_REJECT_UTTERANCE',
+ };
+ }
+ if (role === 'approver' && state === 'approving') {
+ return {
+ kind: 'deny_text',
+ intentTag: '💬 请明确意图',
+ replyContent: `您提到了「驳回」。若要执行终审驳回,请明确说明意见;若查询历史驳回原因,请直接询问。`,
+ denyReason: 'AMBIGUOUS_REJECT_UTTERANCE',
+ };
+ }
+
+ // 编制人等:含「驳回」的模糊说法 → 引导查询,不出危险卡
+ return {
+ kind: 'deny_text',
+ intentTag: '📋 驳回相关说明',
+ replyContent: `您当前是「${roleLabel(role)}」。若询问《${title}》被驳回的原因,可直接说明;系统不会在此身份下发起审核驳回确认。\n\n已知驳回意见摘要:${reason}`,
+ denyReason: 'NON_EXECUTOR_AMBIGUOUS',
+ };
+}
+
+/** 是否允许执行 confirm_reject(二次鉴权:仅审核/批准) */
+export function canConfirmReject(role: string, _lifecycleState?: string): boolean {
+ const r = (role || '').toLowerCase();
+ return r === 'reviewer' || r === 'approver';
+}
diff --git a/web/src/engine/markdownToBlock.ts b/web/src/engine/markdownToBlock.ts
index a78fcda5..4a168d84 100644
--- a/web/src/engine/markdownToBlock.ts
+++ b/web/src/engine/markdownToBlock.ts
@@ -264,7 +264,7 @@ export function markdownToBlockAST(
author: meta?.author || '张工',
reviewer: meta?.reviewer || '李主管',
approver: meta?.approver || '王总监',
- summary: meta?.summary || '由智能体小队自动生成的受控交付物',
+ summary: meta?.summary || '由项目助手自动生成的受控交付物',
updatedAt: meta?.updatedAt || new Date().toISOString().replace('T', ' ').substring(0, 16),
rootBlock,
};
diff --git a/web/src/engine/projectTemplateIntegration.test.ts b/web/src/engine/projectTemplateIntegration.test.ts
index 6e8fa490..17f97f6b 100644
--- a/web/src/engine/projectTemplateIntegration.test.ts
+++ b/web/src/engine/projectTemplateIntegration.test.ts
@@ -50,7 +50,7 @@ test('集成验证:项目切换时交付物严格隔离,新建项目即刻
...targetDoc,
isTemplateOnly: false,
lifecycleState: 'drafting',
- summary: '智能体参谋小队已依据输入完成第一版需求摸底梳理。',
+ summary: '项目助手已依据输入完成第一版需求摸底梳理。',
updatedAt: '2026-09-08 20:30',
};
diff --git a/web/src/engine/templateFactory.ts b/web/src/engine/templateFactory.ts
index 10b0e4a2..8a17e39c 100644
--- a/web/src/engine/templateFactory.ts
+++ b/web/src/engine/templateFactory.ts
@@ -43,7 +43,7 @@ export const STANDARD_WORKFLOW_TEMPLATES: TemplateDefinition[] = [
## 1. 临床背景与预期用途
本项目针对微创临床应用场景,计划研制符合法规规范的 {{device_type}}。
-> [待确认] 临床科室、主要适应症与操作痛点待智能体小队与客户进一步摸底确认。
+> [待确认] 临床科室、主要适应症与操作痛点待项目助手与客户进一步摸底确认。
## 2. 客户诉求与关键约束
- 预算目标:待确认
@@ -111,7 +111,7 @@ flowchart TD
## 1. 监管分类界定与依据条目
依据国家药品监督管理局《医疗器械分类目录》,初步判定如下:
-> [合规引证] 待智能体小队依据知识库最新版分类目录条目进行精确比对与推荐。
+> [合规引证] 待项目助手依据知识库最新版分类目录条目进行精确比对与推荐。
## 2. 强制性合规标准矩阵
经查验国家医疗器械强制性国家标准库与行业标准库,本项目预研规划需满足:
@@ -128,7 +128,7 @@ SNR = \\frac{S_{signal}}{\\sqrt{\\sigma_{ambient}^2 + \\sigma_{dark}^2 + \\sigma
$$
## 4. 结论与申报建议
-(待智能体小队检索法规库后填报正式合规结论)。
+(待项目助手检索法规库后填报正式合规结论)。
`,
},
{
diff --git a/web/src/types/block.ts b/web/src/types/block.ts
index 6a7e6f96..755034b0 100644
--- a/web/src/types/block.ts
+++ b/web/src/types/block.ts
@@ -66,4 +66,11 @@ export interface DocumentAST {
visibilityRule?: DocVisibilityRule;
updatedAt: string;
rootBlock: DocumentBlock;
+ /** 运行时编辑租约(小队/人工占用时正文与批注只读) */
+ editLease?: {
+ holderType?: 'squad' | 'human' | '';
+ holderId?: string;
+ expiresAt?: string;
+ };
+ contentSha256?: string;
}
diff --git a/web/src/types/types.test.ts b/web/src/types/types.test.ts
index 61df6c9c..b71a49df 100644
--- a/web/src/types/types.test.ts
+++ b/web/src/types/types.test.ts
@@ -60,6 +60,7 @@ export const testUser: UserProfile = {
export const testSession: ProjectSession = {
sessionId: 'sess-001',
+ userId: 1,
projectId: 1,
title: '4K超高清荧光内窥镜系统',
deviceType: '硬管荧光腹腔内窥镜',
@@ -110,7 +111,7 @@ export const testMessage: ChatMessage = {
id: 'msg-001',
sessionId: 'sess-001',
role: 'assistant',
- content: '参谋小队已完成技术可行性评估',
+ content: '项目助手已完成技术可行性评估',
timestamp: '14:10',
interactiveCard: testInteractiveCard,
};