docs(asl): Upgrade Tool 3 architecture from Fan-out to Scatter+Aggregator (v2.0)
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>
This commit is contained in:
@@ -1,93 +1,54 @@
|
||||
# 工具 3:全文智能提取工作台 V2.0 开发计划
|
||||
|
||||
> **版本:** v1.5(多 Pod SSE 通信 + PKB 数据一致性快照)
|
||||
> **版本:** v2.0(架构转型:Fan-out → 散装派发 + Aggregator 轮询收口)
|
||||
> **创建日期:** 2026-02-22
|
||||
> **更新日期:** 2026-02-23(v1.1 → v1.2 PKB → v1.3 排雷 → v1.4 终极 → v1.4.1 逻辑补丁 → v1.4.2 致命修复 → v1.5 跨实例+快照)
|
||||
> **架构审查:** `docs/03-业务模块/ASL-AI智能文献/06-技术文档/工具3架构审查与研发改进建议.md`
|
||||
> **深度排雷:** `docs/03-业务模块/ASL-AI智能文献/06-技术文档/工具3开发计划深度审查与排雷指南.md`
|
||||
> **终极审查:** `docs/03-业务模块/ASL-AI智能文献/06-技术文档/工具3终极架构审查与研发规范.md`
|
||||
> **更新日期:** 2026-02-24(v1.1→v1.5 见历史补丁 → **v2.0 架构转型**)
|
||||
> **异步任务指南:** `docs/02-通用能力层/散装派发与轮询收口任务模式指南.md`(Level 2,**本模块遵循**)
|
||||
> **Postgres-Only 指南:** `docs/02-通用能力层/Postgres-Only异步任务处理指南.md`(Level 1 底层规范)
|
||||
> **PKB 模块:** `docs/03-业务模块/PKB-个人知识库/00-模块当前状态与开发指南.md`
|
||||
> **Postgres-Only 指南:** `docs/02-通用能力层/Postgres-Only异步任务处理指南.md`
|
||||
> **产品原型:** `docs/03-业务模块/ASL-AI智能文献/00-系统设计/证据整合V2.0/工具3 全文提取产品原型图V2.html`
|
||||
> **数据字典:** `docs/03-业务模块/ASL-AI智能文献/00-系统设计/证据整合V2.0/ASL 工具 3 全文提取数据字典与规范.md`
|
||||
> **模板规范:** `docs/03-业务模块/ASL-AI智能文献/00-系统设计/证据整合V2.0/ASL 工具 3 提取模板管理规范.md`
|
||||
> **OSS 规范:** `docs/04-开发规范/11-OSS存储开发规范.md`
|
||||
> **OSS 规范:** `docs/04-开发规范/11-OSS存储开发规范.md`
|
||||
> **架构审查(历史):** `docs/03-业务模块/ASL-AI智能文献/06-技术文档/` 下 3 份审查文档(v1.1-v1.5 期间产出,部分结论已被 v2.0 架构转型替代)
|
||||
>
|
||||
> ### v1.5 补丁:多 Pod SSE 通信 + PKB 数据一致性快照
|
||||
> ### 🚀 v2.0 架构转型:Fan-out → 散装派发 + Aggregator 轮询收口
|
||||
>
|
||||
> | # | 问题 | 严重度 | 修正方案 | 影响章节 | 归属里程碑 |
|
||||
> |---|------|--------|---------|---------|-----------|
|
||||
> | 1 | SSE `sseEmitter.emit()` 基于内存 EventEmitter,用户连 Pod A 但 Worker 跑在 Pod B → 实时日志零推送 | **关键** | PostgreSQL `NOTIFY/LISTEN` 跨实例广播:Worker 端 `NOTIFY asl_sse_channel`,API 端每 Pod 启动时 `LISTEN` 常驻连接,收到后检查本机是否有该 taskId 的 SSE 客户端并推送 | Task 2.4 | M2 |
|
||||
> | 2 | 提取任务可能持续 50 分钟,期间用户在 PKB 删除/修改文档 → Child Worker 找不到 `storageKey` 崩溃 | **重要** | Manager 派发时一次性快照 `storageKey` + `filename` 到 `AslExtractionResult`,Child Worker 从自身记录读取而非运行时回查 PKB | Task 1.1, Task 2.3 | M1 |
|
||||
> **转型决策依据:** v1.1-v1.5 期间累计发现 12+ 个 Fan-out 分布式 Bug(Last Child Wins 终点丢失、
|
||||
> 乐观锁与重试绞杀、原子递增行锁争用、Sweeper 误杀、NOTIFY/LISTEN 复杂性等),
|
||||
> 经团队评估,Fan-out 的运维复杂度远超创业团队承受能力。
|
||||
> 详见 `散装派发与轮询收口任务模式指南.md` 的成本-收益分析。
|
||||
>
|
||||
> ### v1.4.2 补丁:3 项致命缺陷修复 + 队列命名合规
|
||||
> | 维度 | v1.5 Fan-out(已废弃) | v2.0 散装派发(当前) |
|
||||
> |------|----------------------|---------------------|
|
||||
> | **调度模型** | API → Manager Job → N Child Job → Last Child Wins | API 直接散装派发 N 个独立 Job → Aggregator 轮询收口 |
|
||||
> | **任务完成判定** | 最后一个 Child 原子递增后翻转 Task status | Aggregator 定时 `groupBy` 聚合,`pending=0 && extracting=0` → completed |
|
||||
> | **进度统计** | Task 表 `successCount`/`failedCount` 冗余字段 | 实时 `groupBy COUNT(Result.status)`,Task 表无计数字段 |
|
||||
> | **僵尸清理** | 独立 Sweeper 定时任务 | Aggregator 自带僵尸清理(一个组件两个职责) |
|
||||
> | **跨 Pod 通信** | NOTIFY/LISTEN 广播 SSE 日志 | 纯轮询,无需跨 Pod 通信(SSE 日志为可选增强) |
|
||||
> | **消灭的组件** | — | ~~ExtractionManagerWorker~~、~~Sweeper~~、~~NOTIFY/LISTEN Bridge~~ |
|
||||
> | **消灭的 Bug 类** | — | ~~Last Child Wins 终点丢失~~、~~原子递增行锁争用~~、~~三级嵌套子队列 OOM~~ |
|
||||
> | **设计模式数** | 12+ | 4 个核心模式 |
|
||||
> | **研发红线数** | 13 条 | 9 条 |
|
||||
>
|
||||
> | # | 致命缺陷 | 严重度 | 补丁方案 | 影响章节 |
|
||||
> |---|---------|--------|---------|---------|
|
||||
> | 1 | Fan-out 终点丢失:没有人把 `AslExtractionTask.status` 从 `processing` 更新为 `completed`,前端永远卡在 Step 2 | **致命** | "Last Child Wins" 模式:Child 原子递增后判断 `successCount + failedCount === totalCount`,最后一个 Child 负责翻转状态 | Task 2.3 |
|
||||
> | 2 | 伪幂等 Read-then-Write 反模式:`findUnique` 检查状态后再操作,并发 retry 穿透导致双倍 MinerU/LLM 调用 + 计数双递增 | **致命** | 替换为 Prisma `updateMany({ where: { status: 'pending' } })` 乐观锁,原子抢占 `extracting` 状态 | Task 2.3 |
|
||||
> | 3 | 队列名称使用点号(`asl.extraction.child`)违反《Postgres-Only 指南》§4.1 红线 | **严重** | 全局替换为下划线格式:`asl_extraction_manager`、`asl_extraction_child`、`asl_mineru_extract`、`asl_llm_extract` | 全局 |
|
||||
> | — | `executionLogs` 注释歧义 | 低 | Schema 注释清理:v1.1 已决定不存日志,注释改为更明确的措辞 | Task 1.1 |
|
||||
> **保留的 v1.x 成果(不受架构转型影响):**
|
||||
> - v1.2 PKB 对接方案(ACL 防腐层)
|
||||
> - v1.3 XML 隔离 Prompt、fuzzyQuoteMatch 搜索池扩容、签名 URL 懒加载
|
||||
> - v1.4 双轨制通信(React Query 主驱 + SSE 日志增强)、MinerU 缓存、HITL 解锁、错误分级
|
||||
> - v1.5 PKB 数据一致性快照(移至 API 层执行)
|
||||
>
|
||||
> ### v1.4.1 补丁:3 项隐蔽逻辑漏洞修复
|
||||
> <details>
|
||||
> <summary>📜 v1.1 - v1.5 历史补丁记录(已被 v2.0 架构转型部分替代,点击展开)</summary>
|
||||
>
|
||||
> | # | 漏洞 | 严重度 | 补丁方案 | 影响章节 |
|
||||
> |---|------|--------|---------|---------|
|
||||
> | 1 | fuzzyQuoteMatch 仅搜索 pymupdf4llm 文本,遗漏 MinerU 表格来源的 Quote → 满屏误报红色警告 | **致命** | 搜索池扩容:剥离 MinerU HTML 标签后与 Markdown 拼接再匹配 | Task 2.3 |
|
||||
> | 2 | logBuffer 内存存储在多 Pod 下为空,SSE Hydration 首帧 recentLogs 为空数组 | 中等 | 降级方案:不存历史日志,重连时仅下发进度 + 打印"监控已重连"提示 | Task 2.4 |
|
||||
> | 3 | `asl_extraction_child` 无 teamConcurrency 限制,1000 篇文献 → 1000 个挂起闭包 → Node.js OOM | **严重** | Child 队列加 `teamConcurrency: 10`,其余在 PostgreSQL 中排队 | Task 2.3 |
|
||||
> v1.5: 多 Pod SSE NOTIFY/LISTEN + PKB 快照 → **SSE 部分已被 v2.0 纯轮询替代,PKB 快照保留但移至 API 层**
|
||||
> v1.4.2: Last Child Wins + 乐观锁 + 队列命名 → **Last Child Wins 和原子递增已被 Aggregator 替代,乐观锁(幽灵重试守卫)和队列命名保留**
|
||||
> v1.4.1: fuzzyQuoteMatch 搜索池 + logBuffer 降级 + teamConcurrency → **全部保留**
|
||||
> v1.4: P-Queue→teamConcurrency + 原子递增 + 错误分级 + MinerU 缓存 + HITL + 双轨制 + 计算卸载 → **原子递增已废弃,其余全部保留**
|
||||
> v1.3: PKB ACL + XML 隔离 + Fan-out 扇出 + 签名 URL + SSE Hydration + PKB 复用日志 → **Fan-out 扇出已被散装模式替代,其余全部保留**
|
||||
> v1.2: PKB 对接 → **全部保留**
|
||||
> v1.1: 10 项架构审查修正 → **全部保留**
|
||||
>
|
||||
> ### v1.4 变更:终极架构审查 7 项修正(v1.4 定稿)
|
||||
>
|
||||
> | # | 终极审查问题 | 修正措施 | 影响章节 |
|
||||
> |---|------------|---------|---------|
|
||||
> | 1 | P-Queue 单机限流在多 Pod 下失效 | 废弃 P-Queue,改用 pg-boss `teamConcurrency` 全局队列限流 | Task 2.3 |
|
||||
> | 2 | Fan-out 并发回写父任务 Race Condition | Prisma 原子递增 `{ increment: 1 }` + 事务保障 | Task 2.3 |
|
||||
> | 3 | 永久错误被 pg-boss 盲目重试 3 次 | 异常分级路由:致命错误 return success 停止重试 | Task 2.3 |
|
||||
> | 4 | MinerU 重复解析同一 PDF 浪费算力 | MinerU Clean Data OSS 缓存(Cache-Aside 模式) | Task 2.2 |
|
||||
> | 5 | Quote 标红后医生无路可走(HITL 死锁) | [强制认可] + [手动修改数值] 双按钮解锁 | Task 5.2 |
|
||||
> | 6 | SSE 断开导致主业务流中断 | 双轨制:React Query 轮询驱动主流 + SSE 仅驱动日志流 | Task 4.1 |
|
||||
> | 7 | 红线 2 计算卸载约束未显式标注 | Node.js 禁碰 pymupdf4llm/MinerU 解析,必须路由到外部服务 | Task 2.2 |
|
||||
>
|
||||
> ### v1.3 变更:深度排雷 6 项修正 + Postgres-Only 安全规范(v1.3)
|
||||
>
|
||||
> | # | 排雷问题 | 修正措施 | 影响章节 |
|
||||
> |---|---------|---------|---------|
|
||||
> | 1 | 跨 Schema 直查打破模块边界 | PKB 暴露 `PkbExportService`(ACL 防腐层),ASL 进程内调用获取 DTO | Task 3.3b, 七.4 |
|
||||
> | 2 | 双引擎上下文污染致 LLM 幻觉 | `<FULL_TEXT>` + `<HIGH_FIDELITY_TABLES>` XML 隔离 + 表格优先级指令 | Task 2.1, 2.2 |
|
||||
> | 3 | pg-boss 单 Job 粗粒度致崩溃重做 | Fan-out 扇出模式:父任务派发 N 个子任务(单篇粒度) | Task 2.3, 2.4 |
|
||||
> | 4 | 签名 URL 1 小时过期打断医生心流 | 懒签名(10 分钟有效期)+ 前端 403 自动刷新 | Task 5.3 |
|
||||
> | 5 | SSE 重连后进度条空白 | SSE Hydration on Connect:连接时下发 `sync` 初始事件 | Task 4.1, 2.4 |
|
||||
> | 6 | PKB 复用无用户感知 | Worker 日志高亮 `[System] Fast-path: Reused from PKB` | Task 2.3 |
|
||||
> | — | Postgres-Only 安全规范 | 幂等 upsert + Payload 轻量 + 合理过期时间(强制遵守) | Task 2.3, 全局 |
|
||||
>
|
||||
> ### v1.2 变更:对接 PKB 个人知识库(取消自建 PDF 上传)
|
||||
>
|
||||
> **决策依据:** PKB 模块已完整集成 OSS 存储(`storage.upload()` + `generatePkbStorageKey()`
|
||||
> + `getDocumentSignedUrl()`),且上传后自动通过 pymupdf4llm 提取 Markdown 全文
|
||||
> 存入 `extractedText` 字段。Tool 3 无需重复造轮子。
|
||||
>
|
||||
> | 删除项 | 原计划内容 | 替代方案 |
|
||||
> |--------|-----------|---------|
|
||||
> | ~~Task 2.6~~ | PDF 上传接入 OSS | PKB 已完成,直接读取 |
|
||||
> | ~~Task 3.3~~ | PdfUploadPanel 组件 | 改为 PkbKnowledgeBaseSelector |
|
||||
> | ~~前端 Upload.Dragger~~ | 拖拽上传 PDF | 选择 PKB 知识库 + 勾选文献 |
|
||||
> | ~~pymupdf4llm 运行时~~ | 每篇 PDF 提取全文 | 直接读 PKB `extractedText`(已有) |
|
||||
>
|
||||
> ### 架构审查修正记录(v1.1)
|
||||
>
|
||||
> | # | 审查问题 | 修正措施 | 影响章节 |
|
||||
> |---|---------|---------|---------|
|
||||
> | 1 | 任务进度管理违背 Postgres-Only 规范 | AslExtractionTask 仅存业务元数据,实时进度交 pg-boss/CheckpointService | Task 1.1 |
|
||||
> | 2 | Step 2 日志推送退回轮询 | 改用 SSE 流式推送,复用 `common/streaming/` | Task 4.1 |
|
||||
> | 3 | MinerU 批量并发无控制 | ~~P-Queue 漏斗~~ → v1.4 改 pg-boss `teamConcurrency` | Task 2.3 |
|
||||
> | 4 | Quote 子串匹配过于理想化 | fuzzyQuoteMatch:Unicode 标准化 + Levenshtein ≤5% | Task 2.3, 七.3 |
|
||||
> | 5 | JSONB 字段无节制膨胀 | MinerU HTML 表格压缩存 OSS,DB 仅存 ossKey | Task 1.1 |
|
||||
> | 6 | 自定义 Prompt 无注入防护 | 指令隔离护栏 BEGIN/END + 防逃逸声明 | Task 2.1 |
|
||||
> | 7 | 断点恢复缺失 | 状态驱动路由,首屏读 status 决定渲染步骤 | Task 4.1, 5.1 |
|
||||
> | 8 | Drawer 大表单渲染卡顿 | Collapse 折叠懒渲染 + Ant Design Form shouldUpdate | Task 5.2 |
|
||||
> | 9 | 排期过于乐观 | Sprint 1 跑通主干 → Sprint 2 叠加高级特性 | 六 |
|
||||
> | 10 | 缺乏 E2E 测试 | 新增 Playwright E2E 测试节点 | Task 6.3 |
|
||||
> </details>
|
||||
|
||||
---
|
||||
|
||||
@@ -224,26 +185,19 @@ model AslProjectTemplate {
|
||||
}
|
||||
|
||||
// 提取任务(业务元数据映射表)
|
||||
// ⚠️ 架构审查修正:本表仅存储业务元数据和任务完成后的聚合统计。
|
||||
// 实时进度由 pg-boss + CheckpointService 托管;日志流为 SSE 瞬时推送(阅后即焚),
|
||||
// 不存入任何数据库字段。Schema 中无 executionLogs 字段(v1.4.2 确认删除设计正确)。
|
||||
// 🆕 v1.2:新增 pkbKnowledgeBaseId,文献来源从 PKB 知识库获取
|
||||
// 🚀 v2.0 散装模式:Task 表无 successCount/failedCount,进度由 COUNT(Result) 实时聚合。
|
||||
// Worker 绝不写 Task 表,仅 Aggregator 在收口时更新 status。
|
||||
// 日志流为 SSE 瞬时推送(可选增强),不存入数据库。
|
||||
model AslExtractionTask {
|
||||
id String @id @default(uuid())
|
||||
projectId String
|
||||
templateId String
|
||||
pkbKnowledgeBaseId String? // 🆕 PKB 知识库 ID(文献 PDF 来源)
|
||||
jobId String? @unique // pg-boss job ID,用于关联实时进度
|
||||
pkbKnowledgeBaseId String? // PKB 知识库 ID(文献 PDF 来源)
|
||||
idempotencyKey String? @unique // 🚀 v2.0 DB 级幂等(@unique + P2002 捕获,防并发重复创建)
|
||||
modelName String @default("deepseek-v3")
|
||||
totalCount Int @default(0) // 待提取文献总数(创建时写入)
|
||||
|
||||
// 以下字段仅在任务完成后由 Worker 回写
|
||||
status String @default("pending") // pending | processing | completed | failed
|
||||
successCount Int @default(0)
|
||||
failedCount Int @default(0)
|
||||
totalTokens Int @default(0)
|
||||
totalCost Decimal @default(0)
|
||||
startedAt DateTime?
|
||||
totalCount Int @default(0) // 待提取文献总数(创建时写入,不再变更)
|
||||
status String @default("processing") // processing | completed | failed(仅 Aggregator 变更)
|
||||
createdAt DateTime @default(now())
|
||||
completedAt DateTime?
|
||||
errorMessage String?
|
||||
|
||||
@@ -255,21 +209,21 @@ model AslExtractionTask {
|
||||
}
|
||||
|
||||
// 单篇文献的提取结果
|
||||
// 🆕 v1.2:新增 pkbDocumentId,支持从 PKB 文档直接读取 extractedText 和 storageKey
|
||||
// 🚀 v2.0:Worker 只写自己的 Result 行,绝不碰 Task 表。
|
||||
// Aggregator 通过 groupBy 聚合 Result.status 来判定 Task 是否完成。
|
||||
model AslExtractionResult {
|
||||
id String @id @default(uuid())
|
||||
taskId String
|
||||
projectId String
|
||||
literatureId String? // 来自工具 1/2 的 AslLiterature(可选)
|
||||
pkbDocumentId String? // 🆕 来自 PKB 的文档 ID(与 literatureId 二选一)
|
||||
pkbDocumentId String? // 来自 PKB 的文档 ID(与 literatureId 二选一)
|
||||
|
||||
// 🆕 v1.5:PKB 数据一致性快照
|
||||
// Manager 派发时冻结,Child Worker 直接使用,无需运行时回查 PKB
|
||||
// 防止提取进行中 PKB 侧删除/修改文档导致任务崩溃
|
||||
snapshotStorageKey String? // 快照:OSS 存储路径(派发时从 PKB 冻结)
|
||||
snapshotFilename String? // 快照:文件名(派发时从 PKB 冻结)
|
||||
// PKB 数据一致性快照(v1.5 设计,v2.0 移至 API 层执行)
|
||||
// API 创建任务时一次性冻结,Worker 直接使用,无需运行时回查 PKB
|
||||
snapshotStorageKey String? // 快照:OSS 存储路径
|
||||
snapshotFilename String? // 快照:文件名
|
||||
|
||||
status String @default("pending") // pending | extracting | extracted | approved | rejected | error
|
||||
status String @default("pending") // pending | extracting | completed | error
|
||||
|
||||
// MinerU 表格提取
|
||||
// ⚠️ 架构审查修正:MinerU HTML 表格可能非常庞大(几 MB),
|
||||
@@ -303,6 +257,7 @@ model AslExtractionResult {
|
||||
literature AslLiterature? @relation(fields: [literatureId], references: [id])
|
||||
// pkbDocumentId 为跨 schema 引用,不建 Prisma relation,通过 PkbBridgeService 查询
|
||||
|
||||
@@index([taskId, status]) // 🚀 v2.0:Aggregator groupBy 聚合加速
|
||||
@@schema("asl_schema")
|
||||
}
|
||||
```
|
||||
@@ -483,88 +438,78 @@ model AslLiterature {
|
||||
|
||||
---
|
||||
|
||||
#### Task 2.3 — 智能提取服务(核心)— ⚠️ v1.3 Fan-out 扇出模式 + Postgres-Only 安全规范
|
||||
#### Task 2.3 — 智能提取服务(核心)— 🚀 v2.0 散装派发 + Aggregator 轮询收口
|
||||
|
||||
**文件:** `backend/src/modules/asl/extraction/services/ExtractionService.ts`
|
||||
**Worker:** `backend/src/modules/asl/extraction/services/ExtractionWorker.ts`
|
||||
**Worker:** `backend/src/modules/asl/extraction/services/ExtractionSingleWorker.ts`
|
||||
**Aggregator:** `backend/src/modules/asl/extraction/services/ExtractionAggregator.ts`
|
||||
|
||||
> **🚨 v1.3 排雷修正(隐患 3):** 原计划将 N 篇文献打包在一个 `pg-boss` Job 中处理,
|
||||
> 如果第 80 篇崩溃,前 79 篇的 `CheckpointService` 数据无法独立恢复(pg-boss 整体 retry 会从头跑)。
|
||||
> 改为 **Fan-out 扇出模式**:Manager Job → N 个 Child Job(单篇粒度),每个 Child 独立 retry。
|
||||
>
|
||||
> **同时强制遵守 `Postgres-Only异步任务处理指南` 的 6 项安全规范。**
|
||||
> **🚀 v2.0 架构转型:** 废弃 Fan-out 扇出模式(Manager → N Child → Last Child Wins),
|
||||
> 改用**散装派发 + Aggregator 轮询收口**模式。
|
||||
> API 层直接创建 N 个独立 Job,Worker 只管自己的 Result 行,Aggregator 定时收口。
|
||||
> 详见 `散装派发与轮询收口任务模式指南.md`。
|
||||
|
||||
**Fan-out 架构图:**
|
||||
**散装派发架构图:**
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ POST /api/v1/asl/extraction/tasks │
|
||||
│ → 创建 AslExtractionTask (DB) │
|
||||
│ → pg-boss.send('asl_extraction_manager', { taskId }) │
|
||||
│ ↓ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ Manager Job (asl_extraction_manager) │ │
|
||||
│ │ 1. 读取任务关联的 N 篇文献 │ │
|
||||
│ │ 2. ⚠️ v1.5 批量快照 PKB 元数据 → 冻结到 │ │
|
||||
│ │ AslExtractionResult.snapshotStorageKey/Filename │ │
|
||||
│ │ 3. 为每篇文献 dispatch Child Job │ │
|
||||
│ │ pg-boss.send('asl_extraction_child', { │ │
|
||||
│ │ taskId, resultId, pkbDocumentId │ │
|
||||
│ │ }) │ │
|
||||
│ │ 4. 派发完毕后 Manager 退出(Fire-and-forget) │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ ↓ (N 个) │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ Child Job (asl_extraction_child) × N │ │
|
||||
│ │ 1. ⚠️ v1.4.2 乐观锁抢占 pending → extracting │ │
|
||||
│ │ 2. 读 PKB extractedText(零成本)+ 用快照字段 │ │
|
||||
│ │ snapshotStorageKey 访问 OSS(防 PKB 删除) │ │
|
||||
│ │ 3. 派发 asl_mineru_extract 子队列(teamConc: 2) │ │
|
||||
│ │ 4. 组装 Prompt → LLM 调用 → fuzzyQuoteMatch │ │
|
||||
│ │ 5. 事务: upsert Result + 原子递增父任务计数 │ │
|
||||
│ │ 6. ⚠️ v1.4.2 "Last Child Wins":判断完成数 = │ │
|
||||
│ │ totalCount → 翻转 Task status = completed │ │
|
||||
│ │ 7. SSE 推送进度日志 │ │
|
||||
│ │ ⚡ 致命错误 return success 停止重试 │ │
|
||||
│ │ ⚡ 临时错误 throw → pg-boss 指数退避自动 retry │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
┌─ API 层 ──────────────────────────────────────┐
|
||||
│ POST /api/v1/asl/extraction/tasks │
|
||||
│ 1. DB 级幂等(idempotencyKey @unique + P2002)│
|
||||
│ 2. 创建 Task(status: processing) │
|
||||
│ 3. 批量创建 N 个 Result(status: pending) │
|
||||
│ 4. PKB 快照冻结(snapshotStorageKey/Filename)│
|
||||
│ 5. 散装派发 N 个 pgBoss Job(一次 insert) │
|
||||
│ 6. 立即返回 taskId(< 0.1 秒) │
|
||||
└────────────────────────────────────────────────┘
|
||||
↓ (N 个独立 Job)
|
||||
┌─ Worker 层(多 Pod 各自抢单)─────────────────┐
|
||||
│ 每个 Worker 只管自己的 1 篇文献: │
|
||||
│ 1. 幽灵重试守卫(updateMany where: pending) │
|
||||
│ 2. 读 PKB extractedText + snapshotStorageKey │
|
||||
│ 3. MinerU 表格提取(含 OSS 缓存) │
|
||||
│ 4. 组装 Prompt → LLM 调用 → fuzzyQuoteMatch │
|
||||
│ 5. 只更新自己的 Result 行 │
|
||||
│ 6. 绝不触碰父任务 Task 表! │
|
||||
│ 7. 错误分级:致命 return / 临时回退 + throw │
|
||||
└────────────────────────────────────────────────┘
|
||||
|
||||
┌─ Aggregator(全局唯一,每 10 秒)─────────────┐
|
||||
│ 1. 清理僵尸:extracting > 30min → error │
|
||||
│ 2. 聚合统计:groupBy Result.status │
|
||||
│ 3. 收口判定:pending=0 且 extracting=0 │
|
||||
│ → 标记 Task completed │
|
||||
└────────────────────────────────────────────────┘
|
||||
|
||||
┌─ 前端(React Query 轮询)─────────────────────┐
|
||||
│ GET /tasks/:taskId/status │
|
||||
│ → 实时 groupBy 聚合进度 │
|
||||
│ → 无需 successCount/failedCount 冗余字段 │
|
||||
└────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
> 📖 代码模式见 08d-工具3-代码模式与技术规范.md §4.1
|
||||
|
||||
> 📖 Manager/Child Worker 代码见 08d §4.2 + §4.3
|
||||
> 📖 代码模式见 08d-工具3-代码模式与技术规范.md §4
|
||||
|
||||
**⚠️ 强制遵守:Postgres-Only 异步任务处理指南安全规范**
|
||||
|
||||
| 规范 | 要求 | 本模块实现 |
|
||||
|------|------|-----------|
|
||||
| **幂等性** | Worker 必须容忍 pg-boss 重投(at-least-once) | ⚠️ v1.4.2 改为 `updateMany({ where: { status: 'pending' } })` 乐观锁原子抢占,替代 Read-then-Write 反模式 |
|
||||
| **Payload 轻量** | Job data 不超过数 KB,禁止塞 PDF 正文 | 仅传 `{ taskId, resultId, pkbDocumentId }`,不超过 200 bytes。快照数据存在 `AslExtractionResult` 表中(v1.5),不塞 Job |
|
||||
| **过期时间** | 必须设置 `expireInMinutes`,防止僵尸 Job | Manager: 60min,Child: 30min |
|
||||
| **错误分级** | 区分"可重试"和"永久失败" | 429/5xx → retry(pg-boss 指数退避),4xx/解析错误 → 标记 error,不 retry |
|
||||
| **死信处理** | 超过 retryLimit 的 Job 进入 DLQ | pg-boss 内置 `onFail` handler 标记该篇为 `error` |
|
||||
| **进度追踪** | 不在 Job data 中存大量进度 | 进度统一走 `CheckpointService`,Job data 仅含 ID 引用 |
|
||||
| **幂等性** | Worker 必须容忍 pg-boss 重投(at-least-once) | 幽灵重试守卫 `updateMany({ where: { status: 'pending' } })`,只允许 pending→extracting |
|
||||
| **API 幂等** | 防止网络重试创建重复 Task | `idempotencyKey @unique` + P2002 捕获(DB 物理级幂等) |
|
||||
| **Payload 轻量** | Job data 不超过数 KB,禁止塞 PDF 正文 | 仅传 `{ resultId, pkbDocumentId }`,不超过 200 bytes |
|
||||
| **过期时间** | 必须设置 `expireInMinutes`,防止僵尸 Job | `expireInMinutes: 30`(执行超时,非排队等待超时) |
|
||||
| **错误分级** | 区分"可重试"和"永久失败" | 致命错误 return(停止重试)/ 临时错误回退 pending + throw(指数退避) |
|
||||
| **死信处理** | 超过 retryLimit 的 Job 进入 DLQ | pg-boss 内置 `onFail` + Aggregator 僵尸清理双保险 |
|
||||
| **Worker 独立性** | Worker 只写自己的行 | **绝不碰 Task 表**,进度由 Aggregator groupBy 聚合 |
|
||||
|
||||
**⚠️ v1.4 终极修正:废弃 P-Queue,改用 pg-boss `teamConcurrency` 全局限流**
|
||||
**并发控制:**
|
||||
|
||||
> **❌ 旧方案(已废弃):** `ExtractionService` 内部使用 `P-Queue({ concurrency: 2 })` 控制 MinerU 并发。
|
||||
> 在多实例(K8s Pods)部署下,每个 Pod 各自有一个 P-Queue,实际全局并发 = 2 × Pod 数,
|
||||
> MinerU API 瞬间 429 熔断 + 重试风暴。
|
||||
>
|
||||
> **✅ 新方案:** 把并发控制权交还给数据库。将 MinerU 解析拆为独立 pg-boss 子队列,
|
||||
> 配置 `teamConcurrency`——这是 PostgreSQL 级全局锁,跨所有 Node.js 实例生效。
|
||||
```
|
||||
asl_extract_single (teamConcurrency: 10) ← 全局限流,防 OOM
|
||||
```
|
||||
|
||||
> 📖 Worker 注册与三级限流见 08d-工具3-代码模式与技术规范.md §4.4
|
||||
|
||||
> **v1.4.2 三级限流架构(队列名已合规):**
|
||||
> ```
|
||||
> asl_extraction_child (teamConcurrency: 10) ← 背压阀门,防 OOM
|
||||
> └─ asl_mineru_extract (teamConcurrency: 2) ← 昂贵 API 保护
|
||||
> └─ asl_llm_extract (teamConcurrency: 5) ← LLM 并发保护
|
||||
> ```
|
||||
> 全部基于 PostgreSQL 行锁实现全局并发控制,跨所有 Node.js 实例生效。
|
||||
> P-Queue 是进程内信号量,多实例下形同虚设——已废弃。
|
||||
> 🚀 v2.0 简化:废弃三级嵌套子队列(`asl_mineru_extract`、`asl_llm_extract`),
|
||||
> 统一为单一队列 `asl_extract_single`,MinerU 和 LLM 调用在 Worker 内部串行执行。
|
||||
> `teamConcurrency: 10` 已足够控制全局并发(10 个 Worker × 1 MinerU/LLM = 最多 10 并发外部调用)。
|
||||
|
||||
**LLM 调用策略:**
|
||||
- 主模型:DeepSeek-V3(128K 上下文,成本低,JSON 输出能力强)
|
||||
@@ -649,28 +594,18 @@ SSE 端点在客户端首次连接时,立即下发一个 `sync` 事件,包
|
||||
|
||||
> 📖 前端 sync 降级处理见 08d-工具3-代码模式与技术规范.md §7.4
|
||||
|
||||
Worker 中通过 `CheckpointService` 更新进度,SSE 端点监听 checkpoint 变化并推送给前端。
|
||||
**🚀 v2.0 SSE 简化:纯轮询为主,SSE 日志流为可选增强**
|
||||
|
||||
**🆕 v1.5:SSE 跨实例实时日志通信 — PostgreSQL NOTIFY/LISTEN**
|
||||
|
||||
> **物理限制:** `sseEmitter.emit()` 基于内存 EventEmitter,用户连 Pod A,但 Worker 跑在 Pod B。
|
||||
> Pod B 触发 emit,Pod A 的 SSE 连接收不到任何实时日志。v1.4.1 仅解决了历史日志(logBuffer 降级),
|
||||
> **实时日志流本身** 在多 Pod 下仍然是断裂的。
|
||||
> v1.5 设计了 PostgreSQL NOTIFY/LISTEN 跨 Pod 广播方案。
|
||||
> v2.0 散装模式以**纯轮询**为核心(React Query 3s 轮询 + groupBy 进度查询),
|
||||
> 无需跨 Pod 实时通信即可满足业务需求。
|
||||
>
|
||||
> **解决方案:** 使用 PostgreSQL `NOTIFY/LISTEN` 实现跨实例广播(Postgres-Only 合规,不引入 Redis)。
|
||||
> **SSE 日志流定位降级为"可选增强":**
|
||||
> - M1:不做 SSE,纯轮询即可
|
||||
> - M2:如果团队有余力,可接入 NOTIFY/LISTEN 做终端日志流(锦上添花)
|
||||
> - 即使 SSE 完全不做,前端进度条和步骤跳转也不受任何影响(双轨制 v1.4 已保障)
|
||||
|
||||
| 角色 | 职责 |
|
||||
|------|------|
|
||||
| **Worker 发送端(Pod B)** | Child Worker 在生成日志时执行 `NOTIFY asl_sse_channel, '{taskId, type, data}'` |
|
||||
| **API 接收端(所有 Pod)** | Pod 启动时建立一条**独立的长连接**(不从连接池借),执行 `LISTEN asl_sse_channel`,收到消息后检查本机是否有该 `taskId` 的 SSE 客户端,有则推送 |
|
||||
|
||||
**关键约束:**
|
||||
- NOTIFY payload 上限 **8000 bytes**(日志消息绰绰有余)
|
||||
- LISTEN 连接必须**独立于连接池**(归还连接后 LISTEN 失效)
|
||||
- NOTIFY 是 fire-and-forget(无持久化、无重放),与 v1.4 双轨制定位完全吻合——日志流本身就是"阅后即焚"的视觉增强
|
||||
- **归属 M2**(M1 不做 SSE 日志流)
|
||||
|
||||
> 📖 NOTIFY/LISTEN 代码模式见 08d-工具3-代码模式与技术规范.md §7.6
|
||||
> 📖 如需实现 SSE 跨 Pod 广播,参见 08d-工具3-代码模式与技术规范.md §7.6(保留为可选方案)
|
||||
|
||||
---
|
||||
|
||||
@@ -1073,11 +1008,11 @@ backend/src/modules/asl/
|
||||
│ │ ├── DynamicPromptBuilder.ts # 动态 Prompt 组装(XML 隔离 + 防注入护栏)
|
||||
│ │ ├── PdfProcessingPipeline.ts # MinerU 表格增强 + PKB 文本复用
|
||||
│ │ ├── PkbBridgeService.ts # PKB 桥接服务(调用 PkbExportService ACL)
|
||||
│ │ ├── ExtractionService.ts # 核心提取服务(teamConcurrency 全局限流)
|
||||
│ │ ├── ExtractionService.ts # 核心提取服务
|
||||
│ │ ├── ExtractionValidator.ts # 提取结果验证(fuzzyQuoteMatch)
|
||||
│ │ ├── ExtractionExcelExporter.ts # Excel 宽表导出
|
||||
│ │ ├── ExtractionManagerWorker.ts # 🆕 v1.3 Fan-out Manager(派发 N 个 Child)
|
||||
│ │ └── ExtractionChildWorker.ts # 🆕 v1.3 Fan-out Child(单篇粒度提取)
|
||||
│ │ ├── ExtractionSingleWorker.ts # 🚀 v2.0 独立 Worker(单篇粒度,只写自己的 Result)
|
||||
│ │ └── ExtractionAggregator.ts # 🚀 v2.0 轮询收口(僵尸清理 + 完成判定)
|
||||
│ ├── prompts/
|
||||
│ │ ├── system_prompt_v2.md # V2 系统提示词
|
||||
│ │ └── extraction_schema/ # 各模板的 JSON Schema
|
||||
@@ -1150,7 +1085,7 @@ frontend-v2/src/modules/asl/
|
||||
|
||||
| 里程碑 | 文档 | 时间 | 核心交付 |
|
||||
|--------|------|------|---------|
|
||||
| **M1 骨架管线** | `08a-工具3-M1-骨架管线冲刺清单.md` | Week 1(5-6 天) | Fan-out 全链路 + PKB ACL + 纯文本盲提 + 极简前端 |
|
||||
| **M1 骨架管线** | `08a-工具3-M1-骨架管线冲刺清单.md` | Week 1(5-6 天) | 散装派发 + Aggregator 全链路 + PKB ACL + 纯文本盲提 + 极简前端 |
|
||||
| **M2 HITL 工作台** | `08b-工具3-M2-HITL工作台冲刺清单.md` | Week 2-3(8-9 天) | MinerU + 审核抽屉 + SSE 日志 + Excel 导出 |
|
||||
| **M3 动态模板引擎** | `08c-工具3-M3-动态模板引擎冲刺清单.md` | Week 4(5-6 天) | 自定义字段 + 安全护栏 + E2E 封版 |
|
||||
| **合计** | | **~22 天** | 每周五可独立 Demo |
|
||||
@@ -1251,22 +1186,15 @@ fileUrl = await storage.upload(storageKey, file); // ✅ 已集成 storage.uplo
|
||||
| 🆕 v1.3 跨模块耦合蔓延 | 未来 PKB 改表导致 ASL 崩溃 | ACL 防腐层:PkbExportService 返回 DTO,ASL 不 import PKB 内部类型 |
|
||||
| 🆕 v1.3 签名 URL 过期 403 | 医生审核中途无法查看 PDF | 懒签名(10 分钟)+ 前端 403 自动刷新 |
|
||||
| 🆕 v1.3 SSE 重连后进度空白 | 用户刷新页面看到空白进度条 | SSE Hydration on Connect:首帧 `sync` 事件下发当前快照 |
|
||||
| 🆕 v1.3 pg-boss 单 Job 崩溃全重做 | N 篇文献因 1 篇崩溃全部重来 | Fan-out 扇出模式:每篇独立 Child Job,独立 retry |
|
||||
| 🆕 v1.3 pg-boss retry 产生重复数据 | 同一篇文献被提取多次 | ⚠️ v1.4.2 改为 `updateMany` 乐观锁抢占 + singletonKey 防重复派发 |
|
||||
| 🆕 v1.4 P-Queue 多实例并发翻倍 | K8s 3 Pod → 实际并发 6 导致 429 | 废弃 P-Queue,改 pg-boss `teamConcurrency`(DB 级全局锁) |
|
||||
| 🆕 v1.4 Fan-out 并发回写计数丢失 | 100 子任务并发 +1 产生 Race Condition | Prisma 原子递增 `{ increment: 1 }` + 事务保障 |
|
||||
| 🆕 v1.4 永久错误被盲目重试 3 次 | PKB 文件删除 → pg-boss 白重试 | 错误分级路由:致命错误 return success 停止重试 |
|
||||
| 🆕 v1.4 MinerU 重复解析浪费算力 | retry / 跨任务同 PDF 重复调用 | MinerU Clean Data OSS 缓存(Cache-Aside) |
|
||||
| 🆕 v1.4 Quote 标红后 HITL 死锁 | 医生无法放行正确但低置信度的提取 | [强制认可] + [手动修改数值] 双按钮解锁 |
|
||||
| 🆕 v1.4 SSE 断开中断主业务流 | 进度条停滞、步骤跳转失效 | 双轨制:React Query 轮询驱动主流 + SSE 仅驱动日志 |
|
||||
| 🚨 v1.4.1 Quote 验证搜索范围遗漏 MinerU | LLM 引用 MinerU 表格文本但验证仅搜索 pymupdf4llm → 满屏红色误报 | `buildQuoteSearchScope()` 拼接 MinerU 纯文本后再匹配 |
|
||||
| 🚨 v1.4.1 logBuffer 多 Pod 内存不共享 | SSE 重连被路由到其他 Pod → 日志区空白 | 降级方案:不存历史日志,重连时打印"监控已重连"提示 |
|
||||
| 🚨 v1.4.1 Child Worker 无并发限制 OOM | 1000 篇文献 → 1000 个挂起闭包 → V8 堆溢出 | `teamConcurrency: 10` 背压限流,其余在 PostgreSQL 排队 |
|
||||
| 🚨 v1.4.2 Fan-out 终点丢失 | Manager 退出后无人翻转 Task status → 前端永远卡在 Step 2 processing | "Last Child Wins":Child 原子递增后判断 `successCount + failedCount >= totalCount` 则翻转 `completed` |
|
||||
| 🚨 v1.4.2 伪幂等 Read-then-Write 穿透 | 并发 retry 同时 `findUnique` 读到 pending → 双倍 MinerU/LLM 调用 + 计数双递增 | `updateMany({ where: { status: 'pending' } })` 乐观锁原子抢占,`count=0` 直接退出 |
|
||||
| 🚨 v1.4.2 队列名称点号违规 | pg-boss 底层将点号识别为 Schema 分隔符,可能路由截断 | 全局替换为下划线格式:`asl_extraction_manager`、`asl_extraction_child`、`asl_mineru_extract`、`asl_llm_extract` |
|
||||
| 🆕 v1.5 SSE 实时日志跨 Pod 断裂 | 用户连 Pod A,Worker 跑 Pod B,`sseEmitter.emit()` 基于内存 EventEmitter → Pod A 零日志 | PostgreSQL `NOTIFY/LISTEN` 跨实例广播,Pod 启动时建立独立长连接 `LISTEN`,Worker 端 `NOTIFY`(归属 M2) |
|
||||
| 🆕 v1.5 PKB 数据提取中被删改 | 50 分钟提取窗口内用户在 PKB 删除 PDF → Child Worker 找不到 `storageKey` 批量崩溃 | Manager 派发时快照 `storageKey` + `filename` 到 `AslExtractionResult`,Child 从自身记录读取(归属 M1) |
|
||||
| 🚀 v2.0 pg-boss retry 产生重复处理 | 同一篇文献被提取多次,浪费 LLM 费用 | 幽灵重试守卫 `updateMany({ where: { status: 'pending' } })`,幽灵 retry 直接跳过 |
|
||||
| 🚀 v2.0 Worker OOM 崩溃后 Result 卡 extracting | Aggregator 前该 Result 永远不收口 | Aggregator 每 10 秒扫描 `extracting > 30min` → 强制标记 error |
|
||||
| 🚀 v2.0 API 重试创建重复 Task | 浏览器重试 / 网络超时重发 | `idempotencyKey @unique` + P2002 捕获(DB 物理级幂等) |
|
||||
| v1.4 永久错误被盲目重试 3 次 | PKB 文件删除 → pg-boss 白重试 | 错误分级路由:致命错误 return 停止重试 |
|
||||
| v1.4 MinerU 重复解析浪费算力 | retry / 跨任务同 PDF 重复调用 | MinerU Clean Data OSS 缓存(Cache-Aside) |
|
||||
| v1.4 Quote 标红后 HITL 死锁 | 医生无法放行正确但低置信度的提取 | [强制认可] + [手动修改数值] 双按钮解锁 |
|
||||
| v1.4 SSE 断开中断主业务流 | 进度条停滞、步骤跳转失效 | 双轨制:React Query 轮询驱动主流 + SSE 仅驱动日志 |
|
||||
| v1.4.1 Quote 验证搜索范围遗漏 MinerU | LLM 引用 MinerU 表格文本但验证仅搜索 pymupdf4llm → 满屏红色误报 | `buildQuoteSearchScope()` 拼接 MinerU 纯文本后再匹配 |
|
||||
| v1.5 PKB 数据提取中被删改 | 50 分钟提取窗口内用户在 PKB 删除 PDF → Worker 找不到 `storageKey` 批量崩溃 | API 创建任务时快照 `storageKey` + `filename` 到 `AslExtractionResult`,Worker 从自身记录读取 |
|
||||
|
||||
---
|
||||
|
||||
@@ -1286,7 +1214,8 @@ fileUrl = await storage.upload(storageKey, file); // ✅ 已集成 storage.uplo
|
||||
| **架构审查与改进建议** | `docs/03-业务模块/ASL-AI智能文献/06-技术文档/工具3架构审查与研发改进建议.md` |
|
||||
| **🆕 深度审查与排雷指南** | `docs/03-业务模块/ASL-AI智能文献/06-技术文档/工具3开发计划深度审查与排雷指南.md` |
|
||||
| **🆕 终极架构审查与研发规范** | `docs/03-业务模块/ASL-AI智能文献/06-技术文档/工具3终极架构审查与研发规范.md` |
|
||||
| **🆕 Postgres-Only 异步任务处理指南** | `docs/02-通用能力层/Postgres-Only异步任务处理指南.md` |
|
||||
| **🚀 散装派发与轮询收口指南** | `docs/02-通用能力层/散装派发与轮询收口任务模式指南.md`(**v2.0 核心参考**) |
|
||||
| **Postgres-Only 异步任务处理指南** | `docs/02-通用能力层/Postgres-Only异步任务处理指南.md` |
|
||||
| **🆕 PKB 个人知识库状态** | `docs/03-业务模块/PKB-个人知识库/00-模块当前状态与开发指南.md` |
|
||||
| **🆕 M1 骨架管线冲刺清单** | `docs/03-业务模块/ASL-AI智能文献/04-开发计划/08a-工具3-M1-骨架管线冲刺清单.md` |
|
||||
| **🆕 M2 HITL 工作台冲刺清单** | `docs/03-业务模块/ASL-AI智能文献/04-开发计划/08b-工具3-M2-HITL工作台冲刺清单.md` |
|
||||
@@ -1296,19 +1225,15 @@ fileUrl = await storage.upload(storageKey, file); // ✅ 已集成 storage.uplo
|
||||
|
||||
---
|
||||
|
||||
*本文档为架构总纲(v1.5)。排期与任务分配已拆分为 M1/M2/M3 三个独立冲刺清单(见第六章)。*
|
||||
*本文档为架构总纲(v2.0 散装派发 + Aggregator 轮询收口)。排期与任务分配已拆分为 M1/M2/M3 三个独立冲刺清单(见第六章)。*
|
||||
|
||||
*M1 启动前全员必须确认以下研发红线:*
|
||||
*1. **Payload 轻量**:pg-boss Job Data 仅传 ID,禁止塞 PDF 正文或 extractedText*
|
||||
*2. **计算卸载**:Node.js 禁碰 pymupdf4llm/MinerU 解析,必须路由到 Python 微服务或 Cloud API*
|
||||
*3. **过期时间**:Child Job `expireInMinutes: 30`,Manager Job `expireInMinutes: 60`*
|
||||
*4. **三级限流**:Child `teamConcurrency: 10` → MinerU `teamConcurrency: 2` → LLM `teamConcurrency: 5`(全部 pg-boss 全局锁)*
|
||||
*5. **原子递增**:Fan-out Child 完成时必须用 Prisma `{ increment: 1 }` 原子更新父任务计数*
|
||||
*6. **错误分级**:致命错误 return success 停止重试,临时错误 throw 让 pg-boss 自动退避*
|
||||
*7. **ACL 防腐层**:ASL 绝不直接 import PKB 内部类型,统一通过 `PkbExportService` 获取 DTO*
|
||||
*8. **Quote 搜索池**:`fuzzyQuoteMatch` 必须在 pymupdf4llm 全文 **+ MinerU 纯文本** 中匹配,禁止仅搜索 Markdown*
|
||||
*9. **Last Child Wins**:Fan-out 模式中最后一个完成的 Child(无论成功或失败)必须翻转父任务 `status = completed`,否则前端永远卡在 Step 2*
|
||||
*10. **乐观锁抢占**:Child Worker 禁止使用 `findUnique → if check` 模式(Read-then-Write),必须用 `updateMany({ where: { status: 'pending' } })` 原子锁*
|
||||
*11. **队列命名下划线**:所有 pg-boss 队列名使用下划线(`asl_extraction_child`),禁止点号(遵守《Postgres-Only 指南》§4.1)*
|
||||
*12. **PKB 快照冻结**:Manager 派发 Child Job 前必须将 `storageKey` + `filename` 快照到 `AslExtractionResult`,Child Worker 从自身记录读取,禁止运行时回查 PKB(v1.5)*
|
||||
*13. **SSE 跨 Pod 广播**:Worker 日志推送必须经 PostgreSQL `NOTIFY`,禁止依赖内存 EventEmitter 跨实例通信(v1.5,M2 实施)*
|
||||
*1. **Worker 独立性**:Worker 只更新自己的 Result 行,**绝不碰 Task 表**,进度由 Aggregator groupBy 聚合*
|
||||
*2. **幽灵重试守卫**:Worker 禁止 `findUnique → if check`(Read-then-Write),必须用 `updateMany({ where: { status: 'pending' } })` 原子锁*
|
||||
*3. **临时错误回退**:临时错误 throw 前必须 `update({ status: 'pending' })` 释放幽灵守卫*
|
||||
*4. **API DB 级幂等**:`idempotencyKey @unique` + P2002 捕获,禁止 Read-then-Write 幂等*
|
||||
*5. **Payload 轻量**:pg-boss Job Data 仅传 `{ resultId, pkbDocumentId }`,禁止塞 PDF 正文*
|
||||
*6. **计算卸载**:Node.js 禁碰 pymupdf4llm/MinerU 解析,必须路由到 Python 微服务或 Cloud API*
|
||||
*7. **错误分级**:致命错误 return 停止重试,临时错误回退 pending + throw 让 pg-boss 自动退避*
|
||||
*8. **ACL 防腐层**:ASL 绝不直接 import PKB 内部类型,统一通过 `PkbExportService` 获取 DTO*
|
||||
*9. **PKB 快照冻结**:API 创建任务时必须将 `storageKey` + `filename` 快照到 `AslExtractionResult`,Worker 从自身记录读取,禁止运行时回查 PKB*
|
||||
|
||||
Reference in New Issue
Block a user