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>
127 lines
6.6 KiB
Markdown
127 lines
6.6 KiB
Markdown
# **🔬 分布式 Fan-out 指南 (v1.2) 破壁级审查与修订报告**
|
||
|
||
**审查人:** 资深架构师 & Node.js 底层专家
|
||
|
||
**审查对象:** 《分布式 Fan-out 任务模式开发指南 v1.2》
|
||
|
||
**审查深度:** V8 引擎内存栈、Postgres 底层驱动、pg-boss 命名空间
|
||
|
||
**核心结论:** 业务逻辑已彻底闭环!但在**JSON 字符串强截断、长期闲置连接保活、全局唯一键防重**这三个底层物理机制上,存在 3 个会直接导致进程崩溃或静默失效的高危隐患。
|
||
|
||
## **🚨 破壁级漏洞 1:NOTIFY 强截断导致的 JSON.parse 爆栈崩溃**
|
||
|
||
### **❌ 逐行审查发现的问题(位于 模式 6:SSE 跨实例广播)**
|
||
|
||
指南中为了防止超过 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 });
|
||
}
|
||
});
|
||
|
||
## **🚨 破壁级漏洞 2:LISTEN 监听器的“静默死亡” (Silent Connection Drop)**
|
||
|
||
### **❌ 逐行审查发现的问题(位于 模式 6:SSE 跨实例广播)**
|
||
|
||
指南中的 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();
|
||
|
||
## **🚨 破壁级漏洞 3:singletonKey 作用域跨界的“狸猫换太子”**
|
||
|
||
### **❌ 逐行审查发现的问题(位于 五、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 定稿版)。有了这套带底层防线的文档,您的团队在面对任何高并发、高可用挑战时,都能写出真正的“大厂级”工业代码! |