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>
This commit is contained in:
127
docs/09-架构实施/分布式Fan-out指南破壁级审查报告.md
Normal file
127
docs/09-架构实施/分布式Fan-out指南破壁级审查报告.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# **🔬 分布式 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 定稿版)。有了这套带底层防线的文档,您的团队在面对任何高并发、高可用挑战时,都能写出真正的“大厂级”工业代码!
|
||||
154
docs/09-架构实施/分布式Fan-out指南逐行级审查报告.md
Normal file
154
docs/09-架构实施/分布式Fan-out指南逐行级审查报告.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# **🔬 分布式 Fan-out 任务模式开发指南:逐行级审查与修正报告**
|
||||
|
||||
**审查人:** 资深架构师 & 分布式系统专家
|
||||
|
||||
**审查对象:** 《分布式 Fan-out 任务模式开发指南 v1.1》
|
||||
|
||||
**审查深度:** 代码级、变量级、多进程时序推演
|
||||
|
||||
**核心结论:** 整体框架卓越!但在“重试机制与乐观锁的冲突”、“清道夫的误伤”、“空任务死锁”以及“底层 SQL 注入”上,存在 **4 处极其隐蔽且致命的系统级 Bug**。必须修正后方可发布 v2.0。
|
||||
|
||||
## **🚨 致命漏洞 1:“乐观锁”与“重试机制”的互相绞杀 (The Retry Paradox)**
|
||||
|
||||
### **❌ 逐行推演发现的问题(位于 模式 3 与 模式 4)**
|
||||
|
||||
在指南的《模式 3:乐观锁抢占》中,您的代码写道:
|
||||
|
||||
const lock \= await prisma.result.updateMany({
|
||||
where: { id: resultId, status: 'pending' },
|
||||
data: { status: 'processing' },
|
||||
});
|
||||
if (lock.count \=== 0\) return { success: true, note: 'Idempotent skip' };
|
||||
|
||||
在《模式 4:错误分级路由》中,当发生临时错误(如 API 超时)时,您的代码写道:
|
||||
|
||||
// 临时错误 (429/5xx/网络抖动):throw → pg-boss 指数退避自动重试
|
||||
throw error;
|
||||
|
||||
**🔥 灾难时序爆发:**
|
||||
|
||||
1. Child Job 第一次运行,成功拿到锁,数据库 status 变为 **processing**。
|
||||
2. 调用外部大模型,发生网络抖动抛出 Error,走到 catch 块,执行了 throw error。
|
||||
3. pg-boss 捕获异常,决定 10 秒后**重试**这个 Job。
|
||||
4. 10 秒后,Child Job 第二次运行,执行 updateMany({ where: { status: 'pending' } })。
|
||||
5. **致命时刻:** 因为第一次失败时**没有把状态改回 pending**,此时数据库里的状态依然是 processing!
|
||||
6. updateMany 返回 count \=== 0。代码打印 "Idempotent skip",然后直接 return { success: true }。
|
||||
7. **后果:** 这个重试的任务什么都没做就“成功”退出了。它**不会**递增父任务的失败数或成功数,父任务**永远缺少一次计数**,"Last Child Wins" 永远无法触发,整个任务死锁卡住。
|
||||
|
||||
### **✅ 骨灰级修正方案**
|
||||
|
||||
在《模式 4:错误分级路由》的 catch 块中,针对临时错误,**必须在 throw error 释放锁(回退状态)**:
|
||||
|
||||
} catch (error) {
|
||||
if (isPermanentError(error)) {
|
||||
// 永久错误逻辑不变...
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
// 核心补丁:临时错误在交给 pg-boss 重试前,必须释放乐观锁!
|
||||
await prisma.result.update({
|
||||
where: { id: resultId },
|
||||
data: { status: 'pending' } // 让出状态,允许下一次重试抢占
|
||||
});
|
||||
|
||||
throw error; // 继续抛出,触发 pg-boss 退避重试
|
||||
}
|
||||
|
||||
## **🚨 致命漏洞 2:清道夫 (Sweeper) 的“友军之火” (Friendly Fire)**
|
||||
|
||||
### **❌ 逐行推演发现的问题(位于 模式 2:Sweeper 清道夫)**
|
||||
|
||||
指南中建议这样筛选卡死的任务:
|
||||
|
||||
where: {
|
||||
status: 'processing',
|
||||
startedAt: { lt: new Date(Date.now() \- 2 \* 60 \* 60 \* 1000\) }, // 超过 2 小时
|
||||
}
|
||||
|
||||
**🔥 灾难时序爆发:**
|
||||
|
||||
1. 用户提交了一个包含 **500 篇**复杂 PDF 的超级批量任务。
|
||||
2. 系统限流 MinerU teamConcurrency: 2,导致这 500 篇文献正常排队执行,总共需要花费 **3 个小时**才能跑完。
|
||||
3. 跑到第 2 小时零 1 分钟时,任务非常健康,已经完成了 350 篇。
|
||||
4. **致命时刻:** Sweeper 定时任务被唤醒。它发现这个任务的 startedAt 是 2 小时前,不管三七二十一,直接把这个健康运行的巨型任务标记为 failed 并强制收口!
|
||||
|
||||
### **✅ 骨灰级修正方案**
|
||||
|
||||
判断一个任务是否“卡死”,不能看它“什么时候开始 (startedAt)”,而必须看它\*\*“上一次产生进度是什么时候 (updatedAt)”\*\*!
|
||||
|
||||
只要子任务还在不断完成,Task 表的 updatedAt 就会不断被刷新。超过 2 小时没有进度更新的,才是真死机。
|
||||
|
||||
// 修正后的 Sweeper 筛选条件
|
||||
const stuckTasks \= await prisma.task.findMany({
|
||||
where: {
|
||||
status: 'processing',
|
||||
// 核心补丁:使用 updatedAt(最后活跃时间)而非 startedAt
|
||||
updatedAt: { lt: new Date(Date.now() \- 2 \* 60 \* 60 \* 1000\) },
|
||||
},
|
||||
});
|
||||
|
||||
## **🚨 致命漏洞 3:Manager 的“空集合”黑洞 (The Empty Batch Deadlock)**
|
||||
|
||||
### **❌ 逐行推演发现的问题(位于 二、核心架构 Manager Job)**
|
||||
|
||||
架构图中写道:Manager 读取 N 个子项 \-\> for each 派发 \-\> 退出。
|
||||
|
||||
但是!如果因为某种极端业务情况(比如用户传了一个空的列表,或者源数据被过滤后 results.length \=== 0)。
|
||||
|
||||
**🔥 灾难时序爆发:**
|
||||
|
||||
1. Manager 查出文献列表,发现长度为 0。
|
||||
2. for 循环不执行,直接退出。
|
||||
3. **致命时刻:** 因为没有任何 Child Job 被派发,所以永远不会有 Child Job 去触发 "Last Child Wins" 收口逻辑。
|
||||
4. 父任务 Task 将永远停留在 status: 'processing'。
|
||||
|
||||
### **✅ 骨灰级修正方案**
|
||||
|
||||
在 Manager Job 派发子任务之前,必须增加边界拦截:
|
||||
|
||||
// Manager Worker 核心补丁
|
||||
if (items.length \=== 0\) {
|
||||
// 如果没有任何子项,Manager 必须自己充当收口人
|
||||
await prisma.task.update({
|
||||
where: { id: taskId },
|
||||
data: { status: 'completed', completedAt: new Date() }
|
||||
});
|
||||
return; // 直接退出
|
||||
}
|
||||
|
||||
// 继续执行 for 循环派发...
|
||||
|
||||
## **🚨 隐患 4:NOTIFY 的底层 SQL 注入与转义灾难**
|
||||
|
||||
### **❌ 逐行推演发现的问题(位于 模式 6:SSE 跨实例广播)**
|
||||
|
||||
指南中使用了原生的 SQL 拼接执行 NOTIFY:
|
||||
|
||||
await prisma.$executeRawUnsafe(
|
||||
\`NOTIFY sse\_channel, '${safePayload.replace(/'/g, "''")}'\`
|
||||
);
|
||||
|
||||
**🔥 灾难时序爆发:**
|
||||
|
||||
1. 这种通过字符串拼接执行 SQL 的方式,在任何正规后端的代码审计中都会被标为**高危 (Critical)**。
|
||||
2. 虽然加了 .replace 单引号,但在不同编码或遇到特殊换行符、反斜杠 \\ 时,仍然可能导致 PostgreSQL 语法解析错误,甚至引发 SQL 注入。
|
||||
|
||||
### **✅ 骨灰级修正方案**
|
||||
|
||||
**抛弃拼接!使用 PostgreSQL 内置的 pg\_notify 函数配合 Prisma 的参数化查询(Tagged Template Literal)。**
|
||||
|
||||
这不仅彻底免疫 SQL 注入,而且完全不需要手动写 replace 转义:
|
||||
|
||||
const payloadStr \= JSON.stringify({ taskId, type: 'log', data: logEntry });
|
||||
const safePayload \= payloadStr.length \> 7000 ? payloadStr.substring(0, 7000\) \+ '..."}' : payloadStr;
|
||||
|
||||
// 核心补丁:使用内置函数与参数化绑定,绝对安全!
|
||||
await prisma.$executeRaw\`SELECT pg\_notify('sse\_channel', ${safePayload})\`;
|
||||
|
||||
## **🎯 终极审查结论**
|
||||
|
||||
您团队产出的这套《分布式 Fan-out 任务模式开发指南》底子极其优良,这证明了你们的技术选型方向是绝对正确的。
|
||||
|
||||
我指出的这 4 个致命漏洞,属于\*\*“分布式并发架构下极其隐蔽的角落”\*\*。即使是互联网大厂的高级开发,如果没踩过这些坑,单看代码也很难发现。
|
||||
|
||||
请立即要求开发团队将这 4 处“骨灰级补丁”更新到开发指南(升级为 v1.2 或 v2.0)中,然后再指导其他类似任务的开发。修复这些问题后,这套系统才算真正拥有了“抗造”的底气!
|
||||
148
docs/09-架构实施/工具3全量代码级同步审计与修正清单.md
Normal file
148
docs/09-架构实施/工具3全量代码级同步审计与修正清单.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# **🔬 工具 3 终极代码级同步审计与修正清单 (基于 Fan-out v1.2)**
|
||||
|
||||
**审计背景:** 确保《工具 3 开发计划 (v1.4.2)》及其代码模式(08d)完全、无死角地落实了《分布式 Fan-out 开发指南 v1.2》中的所有极端场景防御策略。
|
||||
|
||||
**审计结论:** 理论已同步,但**代码落地存在 4 处断层**。必须修改 08d-代码模式与技术规范.md 中的具体代码片段。
|
||||
|
||||
## **🚨 审计点 1:Child Worker 临时错误重试的“死锁穿透” (必须修改)**
|
||||
|
||||
**🔍 逐行审查发现:**
|
||||
|
||||
在 08d 文档的 §4.3 ExtractionChildWorker 的 catch 块中,针对临时错误的代码目前是:
|
||||
|
||||
// 当前 08d 代码
|
||||
// 临时错误 (429/网络抖动):直接 throw,让 pg-boss 自动指数退避重试
|
||||
throw error;
|
||||
|
||||
**💥 业务危害:**
|
||||
|
||||
这直接违背了 Fan-out 指南 v1.2 的核心补丁!因为上方使用了 updateMany 乐观锁把 AslExtractionResult 的状态改为了 extracting。如果直接 throw,pg-boss 在 10 秒后重试时,数据库里该行还是 extracting,乐观锁 updateMany 会返回 count: 0,导致 Worker 误以为任务已完成而直接 return success。**最终导致父任务 AslExtractionTask 永远少一个计数,彻底卡死在 processing。**
|
||||
|
||||
**✅ 代码修正指令:**
|
||||
|
||||
必须在 ExtractionChildWorker 的 catch 块末尾,throw error 之前,强制释放当前业务表的锁:
|
||||
|
||||
// 修正后的 08d §4.3 代码
|
||||
} catch (error) {
|
||||
if (isPermanentError(error)) {
|
||||
// 致命错误处理逻辑不变...
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
// ⚡ 必须增加的解锁代码:临时错误退避前,回退状态为 pending!
|
||||
await prisma.aslExtractionResult.update({
|
||||
where: { id: resultId },
|
||||
data: { status: 'pending' }
|
||||
});
|
||||
|
||||
// 让出状态后,再抛出异常让 pg-boss 重试
|
||||
throw error;
|
||||
}
|
||||
|
||||
## **🚨 审计点 2:Manager Worker 空文献的“无头挂起” (必须修改)**
|
||||
|
||||
**🔍 逐行审查发现:**
|
||||
|
||||
在 08d 文档的 §4.2 ExtractionManagerWorker 中,代码是:
|
||||
|
||||
// 当前 08d 代码
|
||||
const results \= await prisma.aslExtractionResult.findMany({ where: { taskId: task.id } });
|
||||
for (const result of results) {
|
||||
await pgBoss.send('asl\_extraction\_child', ...);
|
||||
}
|
||||
// Manager 退出
|
||||
|
||||
**💥 业务危害:**
|
||||
|
||||
在工具 3 的业务流中,如果用户在 Step 1 勾选的 PKB 文献因为某种原因(如被其他协作者删除)导致 results.length \=== 0,Manager 会直接退出。因为没有任何 Child 被派发,Last Child Wins 永远不触发,AslExtractionTask 状态永远是 processing,前端进度条永远转圈。
|
||||
|
||||
**✅ 代码修正指令:**
|
||||
|
||||
在 ExtractionManagerWorker 获取到 results 后,必须增加边界拦截:
|
||||
|
||||
// 修正后的 08d §4.2 代码
|
||||
const results \= await prisma.aslExtractionResult.findMany({ where: { taskId: task.id } });
|
||||
|
||||
// ⚡ 必须增加的空集合守卫
|
||||
if (results.length \=== 0\) {
|
||||
await prisma.aslExtractionTask.update({
|
||||
where: { id: task.id },
|
||||
data: { status: 'completed', completedAt: new Date() }
|
||||
});
|
||||
// 触发 SSE 完成事件
|
||||
await prisma.$executeRaw\`SELECT pg\_notify('asl\_extraction\_sse', '{"taskId":"${task.id}","type":"complete"}')\`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 正常循环派发...
|
||||
|
||||
## **🚨 审计点 3:工具 3 专属 Sweeper 的缺位 (必须新增)**
|
||||
|
||||
**🔍 逐行审查发现:**
|
||||
|
||||
《Fan-out 开发指南 v1.2》规定必须有 Sweeper 清道夫。但在《工具 3 开发计划》的所有 Task 清单(M1/M2/M3)中,**完全没有分配开发 Sweeper 的任务**。
|
||||
|
||||
**💥 业务危害:**
|
||||
|
||||
如果没有针对 AslExtractionTask 写具体的清道夫代码,一旦遇到极度变态的 PDF 导致 MinerU 或 pymupdf4llm 的 Node.js 宿主进程 OOM 崩溃,该 Task 会永久挂起在前端工作台,医生无法进行后续操作。
|
||||
|
||||
**✅ 代码修正指令:**
|
||||
|
||||
必须在后端模块初始化时(如 backend/src/modules/asl/extraction/index.ts),专门为工具 3 注册一个清道夫 Worker:
|
||||
|
||||
// ⚡ 必须在工具 3 模块启动时注册
|
||||
async function aslExtractionSweeper() {
|
||||
const stuckTasks \= await prisma.aslExtractionTask.findMany({
|
||||
where: {
|
||||
status: 'processing',
|
||||
// 工具 3 独有逻辑:使用 updatedAt 判断最后活跃时间超 2 小时
|
||||
updatedAt: { lt: new Date(Date.now() \- 2 \* 60 \* 60 \* 1000\) },
|
||||
},
|
||||
});
|
||||
|
||||
for (const task of stuckTasks) {
|
||||
await prisma.aslExtractionTask.update({
|
||||
where: { id: task.id },
|
||||
data: { status: 'failed', errorMessage: 'System timeout (OOM/Crash)', completedAt: new Date() },
|
||||
});
|
||||
}
|
||||
}
|
||||
await jobQueue.schedule('asl\_extraction\_sweeper', '\*/10 \* \* \* \*');
|
||||
await jobQueue.work('asl\_extraction\_sweeper', aslExtractionSweeper);
|
||||
|
||||
## **🚨 审计点 4:SSE 广播代码的安全隐患 (必须修改)**
|
||||
|
||||
**🔍 逐行审查发现:**
|
||||
|
||||
在 08d 的 §4.3 中,Child Worker 完成提取后,依然在使用:
|
||||
|
||||
// 当前 08d 代码
|
||||
this.sseEmitter.emit(taskId, { type: 'log', data: { ... } });
|
||||
|
||||
**💥 业务危害:**
|
||||
|
||||
这还是单机内存的 EventEmitter!在 SAE 多实例(多 Pods)部署下,Pod A 上的用户绝对收不到 Pod B 产生的日志。前端日志流会严重断裂。
|
||||
|
||||
**✅ 代码修正指令:**
|
||||
|
||||
必须将所有 this.sseEmitter.emit 替换为安全的、截断的、参数化的 pg\_notify SQL 注入免疫调用:
|
||||
|
||||
// 修正后的 08d §4.3 代码
|
||||
const logEntry \= { source: 'system', message: \`✅ ${extractResult.filename} extracted\` };
|
||||
const payloadStr \= JSON.stringify({ taskId, type: 'log', data: logEntry });
|
||||
// ⚡ 必须进行的 7000 bytes 安全截断(防 PostgreSQL 报错)
|
||||
const safePayload \= payloadStr.length \> 7000 ? payloadStr.substring(0, 7000\) \+ '..."}' : payloadStr;
|
||||
|
||||
// ⚡ 必须使用的参数化 pg\_notify
|
||||
await prisma.$executeRaw\`SELECT pg\_notify('asl\_extraction\_sse', ${safePayload})\`;
|
||||
|
||||
## **🏁 架构师最终放行许可**
|
||||
|
||||
只要开发团队在编写工具 3 代码时,把上述 **4 段具体的代码** 替换到工程中:
|
||||
|
||||
1. Child Worker catch 释放锁
|
||||
2. Manager Worker 拦截空数组
|
||||
3. 注册 asl\_extraction\_sweeper
|
||||
4. 替换 sseEmitter 为参数化 pg\_notify
|
||||
|
||||
您的系统在抗压能力、容错能力和数据一致性上,将绝对达到顶尖大厂的微服务水准!**这一次,您可以 100% 放心闭眼放行了!祝团队开发顺利!**
|
||||
Reference in New Issue
Block a user