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:
2026-02-24 13:08:29 +08:00
parent dc6b292308
commit 85fda830c2
27 changed files with 2732 additions and 154 deletions

View File

@@ -3,7 +3,7 @@
> **所属:** 工具 3 全文智能提取工作台 V2.0
> **架构总纲:** `08-工具3-全文智能提取工作台V2.0开发计划.md`
> **代码手册:** `08d-工具3-代码模式与技术规范.md`(所有代码模式均在此手册中,开发时按需查阅)
> **建议时间:** Week 15-6 天)
> **建议时间:** Week 15.5-6.5 天,含 v1.6 Sweeper 清道夫 0.5 天)
> **核心目标:** 证明 "PKB 拿数据 → Fan-out 分发 → LLM 盲提 → 数据落库 → 前端看到 completed" 这条管线是通的。
---
@@ -69,13 +69,17 @@
- `PkbBridgeService.ts`:调用 `PkbExportService`,代理所有 PKB 数据访问
**Step C — Fan-out Manager + Child Worker1 天)⚠️ 核心战役:**
- `ExtractionManagerWorker.ts`:读取任务 → ⚠️ v1.5 批量快照 PKB 元数据(`snapshotStorageKey` + `snapshotFilename`)冻结到 `AslExtractionResult` → 为每篇文献 `pgBoss.send('asl_extraction_child', ...)` → 退出Fire-and-forget
- `ExtractionManagerWorker.ts`:读取任务 → 🆕 **v1.6 空集合守卫**`results.length === 0` → 直接 completed ⚠️ v1.5 批量快照 PKB 元数据(`snapshotStorageKey` + `snapshotFilename`)冻结到 `AslExtractionResult` → 为每篇文献 `pgBoss.send('asl_extraction_child', ...)` → 退出Fire-and-forget
- `ExtractionChildWorker.ts` 完整逻辑:
1. **乐观锁抢占**`updateMany({ where: { status: 'pending' }, data: { status: 'extracting' } })`
2. **纯文本降级提取**:从 PKB 读 `extractedText` + 写死 RCT Schema → 调用 DeepSeek
3. **原子递增**:事务内 `update Result + increment Task counts`
4. **Last Child Wins**`successCount + failedCount >= totalCount` → 翻转 `status = completed`
5. **错误分级路由**:致命错误 return / 临时错误 throw
5. **错误分级路由**:致命错误 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 注册(遵守队列命名规范):**
```
@@ -94,9 +98,14 @@ jobQueue.work('asl_extraction_child', { teamConcurrency: 10 }, handler)
- [ ] 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
> 📖 ACL 防腐层设计见架构总纲 Task 3.3b
> 📖 Sweeper、乐观锁释放、空集合守卫代码见 08d §4.2 / §4.3 / §4.6
---
@@ -150,6 +159,9 @@ jobQueue.work('asl_extraction_child', { teamConcurrency: 10 }, handler)
| 5 | Job Payload 仅传 ID< 200 bytes禁止塞 PDF 正文 | pg-boss 阻塞 |
| 6 | ACL 防腐层ASL 不 import PKB 内部类型 | 模块耦合蔓延 |
| 7 | Manager 必须快照 `snapshotStorageKey` + `snapshotFilename`Child 禁止运行时回查 PKB 获取 storageKeyv1.5 | 提取中 PKB 删文档 → 批量崩溃 |
| 8 | 🆕 临时错误 `throw` 前必须 `update({ status: 'pending' })` 释放乐观锁v1.6 | 重试时被"幂等跳过"计数永远缺一票Task 永久卡死 |
| 9 | 🆕 Manager 必须检查 `results.length === 0` 并直接 completedv1.6 | 空文献 → 无 Child → Last Child Wins 死锁 |
| 10 | 🆕 必须注册 `asl_extraction_sweeper` 清道夫(`updatedAt > 2h`,禁止用 `startedAt`v1.6 | 进程 OOM/SIGKILL 后 Task 永久挂起 |
---
@@ -160,6 +172,8 @@ jobQueue.work('asl_extraction_child', { teamConcurrency: 10 }, handler)
✅ 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 日志流