Architecture transformation: - Replace Fan-out (Manager->Child->Last Child Wins) with Scatter+Aggregator pattern - API layer directly dispatches N independent jobs (no Manager) - Worker only writes its own Result row, never touches Task table (zero row-lock) - Aggregator polls groupBy for completion + zombie cleanup (replaces Sweeper) - Reduce red lines from 13 to 9, eliminate distributed complexity Documents updated (10 files): - 08-Tool3 main architecture doc: v2.0 rewrite (schema, Task 2.3/2.4, red lines, risks) - 08d-Code patterns: rewrite sections 4.1-4.6 (API dispatch, SingleWorker, Aggregator) - 08a-M1 sprint: rewrite M1-3 core (Worker+Aggregator), red lines, acceptance criteria - 08b-M2 sprint: simplify SSE (NOTIFY/LISTEN downgraded to P2 optional) - 08c-M3 sprint: milestone table wording update - New: Scatter+Polling Aggregator pattern guide v1.1 (Level 2 cookbook) - New: V2.0 architecture deep review and gap-fix report - Updated: ASL module status, system status, capability layer index Co-authored-by: Cursor <cursoragent@cursor.com>
7.3 KiB
🔍 深度审查与断层修复报告:工具 3 全文提取 (V2.0 散装架构版)
审查人: 资深架构师
审查对象: 08 总纲及 08a/08b/08c/08d 拆分计划文档
审查基准: 《散装派发与轮询收口任务模式指南 v1.0》
总体评估: 战略方向极其正确,但文档间存在严重的自相矛盾。旧版 Fan-out 的代码(Manager、原子递增、独立清道夫)未被彻底清除,导致架构撕裂。必须立即修复 08a、08b 和 08d 文档。
🚨 致命断层 1:Worker 仍在修改父任务(行锁地雷未除)
❌ 发现的问题 (位于 08d 代码手册 §4.3)
在 08 总纲中,您明确写道:“Worker 绝不碰 Task 表,进度由 Aggregator groupBy 聚合”。
但是!在 08d 的 ExtractionChildWorker 代码示例中,依然保留了旧版 Fan-out 的致命逻辑:
// 08d 错误残留代码:
const [_resultUpdate, taskAfterUpdate] = await prisma.$transaction([
prisma.aslExtractionResult.update(...),
prisma.aslExtractionTask.update({ ... data: { successCount: { increment: 1 } } }) // ❌ 严重违规!
]);
// ...
if (taskAfterUpdate.successCount + ... >= totalCount) { ... } // ❌ 严重违规 (Last Child Wins 残留)
危害: 如果开发照抄这段代码,散装架构最核心的优势“无锁并发”将彻底丧失,依然会引发 Lock wait timeout 和死锁!
✅ 修复指令 (修改 08d §4.3)
彻底删除 $transaction、删除 increment,删除 Last Child Wins 判定。Worker 的代码必须极简:
// 修正后的 08d §4.3:纯粹的散装 Worker
// ... 幽灵重试守卫保持不变 ...
const extractResult = await this.extractionService.extractOne(resultId, taskId);
// ✅ 核心:只管更新自己的 Result,绝不碰 Task!
await prisma.aslExtractionResult.update({
where: { id: resultId },
data: { status: 'completed', extractedData: extractResult.data, processedAt: new Date() }
});
// ✅ SSE 广播 (可选)
await broadcastLog(taskId, { source: 'system', message: `✅ ${extractResult.filename} extracted` });
// 结束!没有任何收口逻辑!
🚨 致命断层 2:Manager 的幽灵依然存在
❌ 发现的问题 (位于 08a M1-3 和 08d §4.2)
- 08 总纲明确声明:“消灭的组件:ExtractionManagerWorker”,“API 层直接散装派发 N 个独立 Job”。
- 然而,08a (M1-3) 依然安排了 1 天工期去开发 ExtractionManagerWorker.ts。
- 08d (§4.2) 依然详细保留了 Manager Worker 的完整代码。
危害: 架构撕裂。如果 API 层发了一个 Manager Job,Manager 又去发 Child Job,这就退回到了极其笨重的 Fan-out 模式,且徒增了一次重试崩溃的风险。
✅ 修复指令
- 修改 08a M1-3:划掉 ExtractionManagerWorker.ts 的开发任务。
- 修改 08d §4.2:将此节更名为 §4.2 API 层散装派发 (ExtractionController),并替换为以下代码:
// 修正后的 08d §4.2:API 直接派发
async function createTask(req, reply) {
// ... DB 幂等拦截 (idempotencyKey) ...
// 1. 获取 PKB 快照元数据并冻结到 DB (移至 API 层执行)
const pkbDocs = await pkbBridge.getDocumentsDetail(documentIds);
const resultsData = pkbDocs.map(doc => ({
taskId: task.id,
pkbDocumentId: doc.documentId,
snapshotStorageKey: doc.storageKey,
status: 'pending'
}));
await prisma.aslExtractionResult.createMany({ data: resultsData });
const createdResults = await prisma.aslExtractionResult.findMany({ where: { taskId: task.id } });
// 2. 🚀 极速散装派发
const jobs = createdResults.map(result => ({
name: 'asl_extract_single',
data: { resultId: result.id, taskId: task.id, pkbDocumentId: result.pkbDocumentId },
options: { singletonKey: `extract-${result.id}`, expireInMinutes: 30 }
}));
await jobQueue.insert(jobs); // 批量压入 pg-boss
return reply.send({ taskId: task.id });
}
🚨 致命断层 3:Sweeper(清道夫) 与 Aggregator(收口器) 未合并
❌ 发现的问题 (位于 08a M1-3 和 08d §4.6)
在《散装模式指南》中,你们团队提出了一个绝妙的优化:“Aggregator 兼职 Sweeper,一个组件两个职责”。
但在 08a 中,依然要求单独注册 asl_extraction_sweeper;在 08d 中也保留了独立的 Sweeper 代码。
✅ 修复指令 (修改 08a 和 08d)
- 修改 08a:删除 Step D (Sweeper 注册)。新增任务:“开发 ExtractionAggregator.ts,实现僵尸清理与轮询收口”。
- 修改 08d:删除 §4.6 的独立 Sweeper。补充 ExtractionAggregator 的标准代码:
// 修正后的 08d §4.6:ExtractionAggregator 轮询包工头
jobQueue.work('asl_extraction_aggregator', async () => {
const tasks = await prisma.aslExtractionTask.findMany({ where: { status: 'processing' } });
for (const task of tasks) {
// 1. 顺手清理僵尸 (Sweeper 职责)
await prisma.aslExtractionResult.updateMany({
where: { taskId: task.id, status: 'extracting', updatedAt: { lt: new Date(Date.now() - 30 * 60 * 1000) } },
data: { status: 'error', errorMessage: '[Aggregator] Timeout, likely worker crash.' }
});
// 2\. 聚合统计
const stats \= await prisma.aslExtractionResult.groupBy({ by: \['status'\], where: { taskId: task.id }, \_count: true });
const pending \= stats.find(s \=\> s.status \=== 'pending')?.\_count || 0;
const extracting \= stats.find(s \=\> s.status \=== 'extracting')?.\_count || 0;
// 3\. 收口
if (pending \=== 0 && extracting \=== 0\) {
await prisma.aslExtractionTask.update({ where: { id: task.id }, data: { status: 'completed' } });
}
}
});
⚠️ 体验断层 4:M2-3 对 NOTIFY/LISTEN 的执念
❌ 发现的问题 (位于 08b M2-3)
08 总纲明确将 NOTIFY/LISTEN 跨 Pod 广播降级为“已被 V2.0 纯轮询替代/可选增强”。但在 M2 冲刺清单(08b M2-3)中,依然将 SseNotifyBridge.ts 和独立 PgClient 的开发列为了“必须做的额外任务”。
✅ 修复指令 (修改 08b)
为了保证 M2 的按时交付,请将 08b 中的 "🆕 v1.5 额外任务:SSE 跨 Pod 广播" 直接删除或标注为 [P2 可选]。
在散装架构下,既然进度条和跳页全靠 React Query 轮询,终端日志由于没有严重的业务关联,前期完全可以用“本 Pod 内存事件”将就,没必要在 M2 阶段去死磕复杂的 PG 独立监听连接。
🏁 架构师总结建议
这个断层非常典型。因为我们前期对 Fan-out 模式进行了多达 6 轮的极限推演和打磨,那些代码(乐观锁、原子递增、Manager)已经深入人心,导致在重写 V2.0 文档时,大家潜意识里舍不得删掉它们。
请项目经理做最后一步:
打开 08a、08b 和 08d。凡是带了 Manager、successCount: { increment: 1 }、Last Child Wins、asl_extraction_sweeper 字眼的代码和任务,全部无情删掉,替换为上述的散装代码。
删完之后,你们的系统代码量将减少 30%,但稳定性将提升 200%!恭喜团队,你们即将拥有一套极其优雅的底层流水线,放心开干吧!