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>
9.5 KiB
M1:骨架管线 — The Skeleton Pipeline
所属: 工具 3 全文智能提取工作台 V2.0
架构总纲:08-工具3-全文智能提取工作台V2.0开发计划.md
代码手册:08d-工具3-代码模式与技术规范.md(所有代码模式均在此手册中,开发时按需查阅)
建议时间: Week 1(5.5-6.5 天,含 v1.6 Sweeper 清道夫 0.5 天)
核心目标: 证明 "PKB 拿数据 → Fan-out 分发 → LLM 盲提 → 数据落库 → 前端看到 completed" 这条管线是通的。
Demo 形态
用户在前端点击按钮,系统后台静默跑完流程,前端 useTaskStatus 轮询到 status = completed,数据库能查到 JSON 提取结果。前端只需一个极简列表。
关键妥协:M1 不接 MinerU,不做审核抽屉,不做 SSE 日志流。
任务清单
M1-1:Prisma 数据模型 + Migration + Seed(1 天)
做什么:
- 新增
AslExtractionTemplate、AslProjectTemplate、AslExtractionTask、AslExtractionResult四张表 - 运行
npx prisma migrate dev --name add_extraction_template_engine - Seed 脚本注入 3 套系统内置模板(RCT / Cohort / QC)
验收标准:
npx prisma migrate deploy成功npx prisma db seed后数据库有 3 套模板记录AslExtractionTask含pkbKnowledgeBaseId字段AslExtractionResult含snapshotStorageKey+snapshotFilename快照字段(v1.5)AslExtractionResult含pkbDocumentId字段、status含extracting状态值
📖 Schema 详情见架构总纲 Task 1.1
M1-2:模板 API + 提取任务 API — 仅基座模板(1.5 天)
做什么:
TemplateController.ts:GET 模板列表、GET 模板详情、POST 克隆到项目ExtractionController.ts:POST 创建任务、GET 任务状态(React Query 轮询用)、GET 结果列表- 创建任务时:锁定模板 → 批量创建
AslExtractionResult(status=pending)→pgBoss.send('asl_extraction_manager', { taskId })
不做什么:
- 不做自定义字段 CRUD API(M3)
- 不做 SSE 端点(M2)
- 不做 Excel 导出(M2)
验收标准:
POST /api/v1/asl/extraction/tasks能创建任务并入队GET /api/v1/asl/extraction/tasks/:taskId返回status、successCount、totalCountGET /api/v1/asl/extraction/tasks/:taskId/results返回提取结果列表
📖 端点完整列表见架构总纲 Task 1.3 + Task 2.4
M1-3:PKB ACL 防腐层 + Fan-out 调度核心(2 天)⚠️ 本里程碑最关键
做什么(按顺序):
Step A — PKB 侧 ACL(0.5 天):
PkbExportService.ts(PKB 模块维护):listKnowledgeBases()、listPdfDocuments()、getDocumentForExtraction()返回 DTO- 通过依赖注入暴露给 ASL
Step B — ASL 侧桥接(0.5 天):
PkbBridgeService.ts:调用PkbExportService,代理所有 PKB 数据访问
Step C — Fan-out Manager + Child Worker(1 天)⚠️ 核心战役:
ExtractionManagerWorker.ts:读取任务 → 🆕 v1.6 空集合守卫(results.length === 0→ 直接 completed) → ⚠️ v1.5 批量快照 PKB 元数据(snapshotStorageKey+snapshotFilename)冻结到AslExtractionResult→ 为每篇文献pgBoss.send('asl_extraction_child', ...)→ 退出(Fire-and-forget)ExtractionChildWorker.ts完整逻辑:- 乐观锁抢占:
updateMany({ where: { status: 'pending' }, data: { status: 'extracting' } }) - 纯文本降级提取:从 PKB 读
extractedText+ 写死 RCT Schema → 调用 DeepSeek - 原子递增:事务内
update Result + increment Task counts - Last Child Wins:
successCount + failedCount >= totalCount→ 翻转status = completed - 错误分级路由:致命错误 return / 🆕 v1.6 临时错误 throw 前释放乐观锁(回退 status → pending)
- 乐观锁抢占:
Step D — 🆕 Sweeper 清道夫注册(0.5 天)(v1.6 新增):
asl_extraction_sweeper:pg-boss 定时任务,每 10 分钟扫描processing且updatedAt > 2h的任务,强制标记failed- 使用
updatedAt(最后活跃时间)判断卡死,禁止用startedAt(防误杀健康的超大批量任务)
Worker 注册(遵守队列命名规范):
jobQueue.work('asl_extraction_child', { teamConcurrency: 10 }, handler)
M1 阶段简化:不注册 asl_mineru_extract 子队列(M2 才接 MinerU)。
验收标准:
- PkbExportService 能返回知识库列表和文档详情(DTO)
- Manager 派发后
AslExtractionResult.snapshotStorageKey和snapshotFilename已填充(v1.5 快照验证) - 手动删除 PKB 文档记录后,Child Worker 仍能通过
snapshotStorageKey从 OSS 获取 PDF(v1.5 一致性验证) - Manager 能为 N 篇文献派发 N 个 Child Job
- Child Worker 乐观锁正确:并发重试不会双倍处理
- Child Worker 原子递增:10 篇并发提取后
successCount = 10 - Last Child Wins:最后一个 Child 翻转 Task status = completed
- 致命错误(PKB 文档不存在)→ 该篇标 error + 不重试 + 不阻塞其他篇
- 临时错误(429)→ pg-boss 指数退避重试
- 🆕 临时错误 throw 前回退
status → pending:模拟 429 重试后乐观锁仍能抢占成功(v1.6 乐观锁释放验证) - 🆕 Manager 空集合守卫:
results.length === 0时 Task 直接标记completed(v1.6 边界验证) - 🆕 Sweeper 清道夫已注册:
asl_extraction_sweeper定时任务在 pg-boss 中可查到(v1.6) - 🆕 Sweeper 判定条件为
updatedAt > 2h,而非startedAt(v1.6 防误杀验证)
📖 Fan-out 架构图、Worker 代码模式、研发红线见架构总纲 Task 2.3
📖 ACL 防腐层设计见架构总纲 Task 3.3b
📖 Sweeper、乐观锁释放、空集合守卫代码见 08d §4.2 / §4.3 / §4.6
M1-4:前端极简 Step 1 — 选模板 + 选 PKB 文献(1 天)
做什么:
ExtractionSetup.tsx:左栏模板下拉(只读,默认 RCT)+ 右栏 PKB 知识库下拉 + 文献 Checkbox 列表PkbKnowledgeBaseSelector.tsx:调用 PKB API 加载知识库和文献- 底部 "确认并开始提取" 按钮 → 调用
POST /api/v1/asl/extraction/tasks
不做什么:
- 不做自定义字段 UI(M3)
- 不做基座字段标签云展示(M2 附带做)
验收标准:
- 能选择 PKB 知识库并展示 PDF 文档列表
- 能勾选文献并提交创建任务
- 空知识库时显示引导提示 + PKB 跳转链接
M1-5:前端极简 Step 2 + Step 3 — 轮询进度 + 极简列表(1 天)
做什么:
ExtractionProgress.tsx:useTaskStatus轮询(3s)驱动进度条 + 检测completed跳转ExtractionWorkbench.tsx:极简表格展示提取结果(Study ID、状态)ExtractionPage.tsx:状态驱动路由(pending→Step1 / processing→Step2 / completed→Step3)- 路由注册(前端 + 后端)
不做什么:
- 不做 SSE 日志终端(M2)
- 不做审核抽屉(M2)
- 不做 Excel 导出按钮(M2)
验收标准:
- 进度条从 0% 推进到 100%(React Query 轮询驱动)
status = completed后自动跳转到 Step 3- Step 3 能看到提取结果列表(状态列展示 completed/error)
- 关闭浏览器重新打开 → 恢复到正确步骤(断点恢复)
M1 研发红线(全员必须背诵)
| # | 红线 | 违反后果 |
|---|---|---|
| 1 | 队列名用下划线(asl_extraction_child),禁止点号 |
pg-boss 路由截断 |
| 2 | Child Worker 用 updateMany 乐观锁,禁止 findUnique → if |
并发穿透,算力翻倍 |
| 3 | Last Child Wins 终止器,成功和失败路径都要检查 | Task 永远卡在 processing |
| 4 | teamConcurrency: 10,禁止无限拉取 Child Job |
Node.js OOM |
| 5 | Job Payload 仅传 ID(< 200 bytes),禁止塞 PDF 正文 | pg-boss 阻塞 |
| 6 | ACL 防腐层:ASL 不 import PKB 内部类型 | 模块耦合蔓延 |
| 7 | Manager 必须快照 snapshotStorageKey + snapshotFilename,Child 禁止运行时回查 PKB 获取 storageKey(v1.5) |
提取中 PKB 删文档 → 批量崩溃 |
| 8 | 🆕 临时错误 throw 前必须 update({ status: 'pending' }) 释放乐观锁(v1.6) |
重试时被"幂等跳过",计数永远缺一票,Task 永久卡死 |
| 9 | 🆕 Manager 必须检查 results.length === 0 并直接 completed(v1.6) |
空文献 → 无 Child → Last Child Wins 死锁 |
| 10 | 🆕 必须注册 asl_extraction_sweeper 清道夫(updatedAt > 2h,禁止用 startedAt)(v1.6) |
进程 OOM/SIGKILL 后 Task 永久挂起 |
M1 结束时的状态
✅ Prisma 表 + 3 套 Seed 模板
✅ PKB ACL 防腐层 → PkbExportService + PkbBridgeService
✅ Fan-out 全链路:Manager → N × Child → Last Child Wins → completed
✅ 乐观锁 + 原子递增 + 错误分级路由 — 所有并发 Bug 已验证
✅ 🆕 Sweeper 清道夫注册(v1.6 防 OOM/SIGKILL 卡死)
✅ 🆕 乐观锁释放 + 空集合守卫 + pg_notify 参数化(v1.6 全量代码级同步)
✅ 前端三步走:选模板/选文献 → 轮询进度 → 极简结果列表
❌ 无 MinerU(纯文本降级)
❌ 无 SSE 日志流
❌ 无审核抽屉
❌ 无自定义字段
❌ 无 Excel 导出
M1 的核心价值: 所有分布式 Bug(并发死锁、幂等穿透、终点丢失、背压 OOM)在第一周就被逼出来。M2 加特性时地基是稳的。