Files
AIclinicalresearch/docs/09-架构实施/分布式Fan-out指南破壁级审查报告.md
HaHafeng 85fda830c2 feat(ssa): Complete Phase V-A editable analysis plan variables
Features:
- Add editable variable selection in workflow plan (SingleVarSelect + MultiVarTags)
- Implement 3-layer flexible interception (warning bar + icon + blocking dialog)
- Add tool_param_constraints.json for 12 statistical tools parameter validation
- Add PATCH /workflow/:id/params API with Zod structural validation
- Implement synchronous parameter sync before execution (Promise chaining)
- Fix LLM hallucination by strict system prompt constraints
- Fix DynamicReport object-based rows compatibility (R baseline_table)
- Fix Word export row.map error with same normalization logic
- Restore inferGroupingVar for smart default variable selection
- Add ReactMarkdown rendering in SSAChatPane
- Update SSA module status document to v3.5

Modified files:
- backend: workflow.routes, ChatHandlerService, SystemPromptService, FlowTemplateService
- frontend: WorkflowTimeline, SSAWorkspacePane, DynamicReport, SSAChatPane, ssaStore, ssa.css
- config: tool_param_constraints.json (new)
- docs: SSA status doc, team review reports

Tested: Cohort study end-to-end execution + report export verified
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 13:08:29 +08:00

127 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# **🔬 分布式 Fan-out 指南 (v1.2) 破壁级审查与修订报告**
**审查人:** 资深架构师 & Node.js 底层专家
**审查对象:** 《分布式 Fan-out 任务模式开发指南 v1.2》
**审查深度:** V8 引擎内存栈、Postgres 底层驱动、pg-boss 命名空间
**核心结论:** 业务逻辑已彻底闭环!但在**JSON 字符串强截断、长期闲置连接保活、全局唯一键防重**这三个底层物理机制上,存在 3 个会直接导致进程崩溃或静默失效的高危隐患。
## **🚨 破壁级漏洞 1NOTIFY 强截断导致的 JSON.parse 爆栈崩溃**
### **❌ 逐行审查发现的问题(位于 模式 6SSE 跨实例广播)**
指南中为了防止超过 PostgreSQL 的 8000 bytes 限制,写了如下安全截断代码:
const payloadStr \= JSON.stringify({ taskId, type: 'log', data: logEntry });
const safePayload \= payloadStr.length \> 7000
? payloadStr.substring(0, 7000\) \+ '..."}' // 👈 致命漏洞
: payloadStr;
**🔥 灾难时序爆发:**
1. 假设 logEntry 的内容包含大量报错栈,导致 payloadStr 长达 9000 字节。
2. 代码直接在第 7000 个字符处一刀切,然后粗暴地拼上 ..."}。
3. 如果第 7000 个字符刚好切在一个中文字符的中间(导致 Unicode 乱码),或者切在了 JSON 的某个 key 名字中间(如 {"ms拼接后的字符串将变成**绝对非法的 JSON**。
4. **爆炸点:** 在 API 接收端代码是这样写的const { taskId, type, data } \= JSON.parse(msg.payload);
5. 当非法的 JSON 被 JSON.parse 解析时Node.js 会抛出 SyntaxError: Unexpected token。由于 pgClient.on('notification') 内部没有写 try-catch**这个异常会直接击穿 Event Loop导致整个 API Pod 进程崩溃重启 (Crash)**
### **✅ 架构师修正方案 (内部字段安全截断)**
**绝对不能对 JSON.stringify 后的字符串进行切片!必须切片原始对象内的长文本字段:**
// 发送端:在 Stringify 之前,截断真正导致超长的 message 字段
if (logEntry.message && logEntry.message.length \> 3000\) {
logEntry.message \= logEntry.message.substring(0, 3000\) \+ '...\[Truncated\]';
}
const payloadStr \= JSON.stringify({ taskId, type: 'log', data: logEntry });
await prisma.$executeRaw\`SELECT pg\_notify('sse\_channel', ${payloadStr})\`;
// 接收端:必须加上防御性 try-catch防止毒数据炸毁整个实例
pgClient.on('notification', (msg) \=\> {
try {
const { taskId, type, data } \= JSON.parse(msg.payload);
// ... 推送逻辑
} catch (error) {
logger.error('Failed to parse SSE notification payload', { payload: msg.payload });
}
});
## **🚨 破壁级漏洞 2LISTEN 监听器的“静默死亡” (Silent Connection Drop)**
### **❌ 逐行审查发现的问题(位于 模式 6SSE 跨实例广播)**
指南中的 API 接收端初始化代码如下:
const pgClient \= new Client({ connectionString: DATABASE\_URL });
await pgClient.connect();
await pgClient.query('LISTEN sse\_channel');
**🔥 灾难时序爆发:**
1. 这是一个一直挂在后台的长期长连接Long-lived Connection
2. 在云端环境由于底层网络波动、PgBouncer 代理的闲置超时掐断、或者数据库主备切换,这根 TCP 连接**一定会在几天内断开一次**。
3. pg 原生库的设计是:**Client 连接断开后,不会自动重连!**
4. **后果:** 没有任何报错,服务依然在跑,但这个 Pod **永远也收不到**任何 NOTIFY 消息了。前端 SSE 终端彻底变成一潭死水。
### **✅ 架构师修正方案 (加入心跳重连与错误监听)**
必须为这个裸 Client 加上底层的生命周期守护:
// 封装为健壮的监听器启动函数
async function setupSSEListener() {
const pgClient \= new Client({ connectionString: DATABASE\_URL });
// 核心补丁:监听错误与断开,强制重启监听!
pgClient.on('error', (err) \=\> {
logger.error('PG Listen Client Error, reconnecting...', err);
pgClient.end().catch(console.error);
setTimeout(setupSSEListener, 5000); // 5 秒后自动重连
});
pgClient.on('end', () \=\> {
logger.warn('PG Listen Client Ended, reconnecting...');
setTimeout(setupSSEListener, 5000);
});
await pgClient.connect();
await pgClient.query('LISTEN sse\_channel');
pgClient.on('notification', (msg) \=\> { /\* ... \*/ });
}
setupSSEListener();
## **🚨 破壁级漏洞 3singletonKey 作用域跨界的“狸猫换太子”**
### **❌ 逐行审查发现的问题(位于 五、pg-boss 配置速查)**
指南中建议这样写防重复 Key
await pgBoss.send('module\_task\_child', { taskId, itemId }, {
singletonKey: \`child-${itemId}\`, // ← 派发防重
});
**🔥 灾难时序爆发:**
1. 假设 itemId 指的是源数据的 ID例如 PKB里的 Document A
2. 医生张三在“项目 1”里提取了 Document A。pg-boss 生成了 singletonKey: child-DocA正在缓慢处理。
3. **同时**,医生李四在“项目 2”里也碰巧勾选了同一个 Document A 进行提取任务。
4. pg-boss 会发现 singletonKey: child-DocA 已经在队列里了。根据幂等性去重规则,**pg-boss 会直接丢弃Ignored李四的这个子任务**
5. **后果:** 李四的这个任务永远缺少了这一篇文献的进度,父任务 Last Child Wins 机制卡死,系统挂起!
### **✅ 架构师修正方案 (引入绝对隔离域)**
singletonKey 在 pg-boss 中是**全局数据库唯一**的!它绝不能仅仅绑定业务实体 ID必须绑定\*\*“实体 \+ 本次任务实例”\*\*的联合主键。
如果 itemId 对应的是 AslExtractionResult.id专属于某一次提取任务的单行 ID那是安全的。但为了规范指南必须明确指出
// 核心补丁singletonKey 必须携带父任务 ID 形成绝对隔离!
await pgBoss.send('module\_task\_child', { taskId, itemId }, {
singletonKey: \`task-${taskId}-item-${itemId}\`, // 必须绑定 Task 实例级别!
});
## **🏁 最终判词与交付建议**
经过这次“贴脸”级别的底层审查,我们不仅防住了高并发的业务死锁,还防住了**网络断线、V8 JSON解析、全局哈希碰撞**这三个底层基建级别的灭顶之灾。
请将这 3 个补丁打入《分布式 Fan-out 任务模式开发指南 v1.2》中(升级为 v1.3 定稿版)。有了这套带底层防线的文档,您的团队在面对任何高并发、高可用挑战时,都能写出真正的“大厂级”工业代码!