|
|
@@ -0,0 +1,109 @@
|
|
|
+# Book 43 卡死问题 & 5 层兜底方案
|
|
|
+
|
|
|
+> 2026-07-04 记录
|
|
|
+> 触发问题:`https://book.rrbrr.com/#/pages/book-generator/detail?id=43` 显示"生成中"实际无任何进度,3 天未推进。
|
|
|
+
|
|
|
+## 现象
|
|
|
+
|
|
|
+- 用户访问 book 43 详情页:进度永远 5%,状态"大纲完成",无法继续
|
|
|
+- 前端轮询 + stalledTip 兜底都触发过,但用户没看到任何"请重新生成"按钮
|
|
|
+- 后端日志:book 43 自 7/2 09:51 之后无任何 LangGraph/TTS 活动
|
|
|
+
|
|
|
+## 根因(深度排查后)
|
|
|
+
|
|
|
+1. **interactive 流程未闭环**:用户用 interactive 模式改完大纲(`POST /interactive/outline`),但前端没引导也没暴露"开始生成内容"按钮,导致 `POST /interactive/generate` 从未被调用,book 永远停在 `genStage='outline_ready'`(不在前端合法枚举里)
|
|
|
+2. **LangGraph rich_outline 节点 720s 超时**:book-recovery-scanner 触发 LangGraph 全量重跑时,rich_outline 节点 12 分钟不够(25 章大书),再次失败
|
|
|
+3. **章节状态不一致**:book 字段已 `content_completed`,但 BookChapter 仍是 `outline_completed`,`computeBookGenStage` 推断前端还是"大纲阶段"
|
|
|
+4. **核心业务测试超时设太紧**:poll.ts 默认 180s,生产 LangGraph 跑 1000 字实测 3 分 48 秒,CORE-3 误报失败
|
|
|
+
|
|
|
+## 改动总览(5 层兜底 + 1 个 100% 兜底)
|
|
|
+
|
|
|
+### 1. `nodes/rich-outline.node.ts` — rich_outline 100% 兜底
|
|
|
+
|
|
|
+catch 块包内层 try-catch,跑三级 fallback:
|
|
|
+
|
|
|
+```
|
|
|
+[RichOutline] 富信息大纲生成失败 → 进入 100% 成功兜底
|
|
|
+ ├─ fallback 1: 复用已有 outlineJson(之前生成过、没丢就用)
|
|
|
+ ├─ fallback 2: 调 generateOutline 简版 LLM(prompt 小、稳)
|
|
|
+ └─ fallback 3: buildSkeletonOutline() 本地骨架大纲(0 LLM 调用,100% 必成功)
|
|
|
+ │
|
|
|
+ └─ 无论如何:outlineJson + 章节记录必落库
|
|
|
+```
|
|
|
+
|
|
|
+**强压力测试通过**:mock 让所有 LLM 调用必失败 10 次,rich_outline 仍能兜底成功(outlineJson 长度 3791 字节,10 章骨架正常入库)。
|
|
|
+
|
|
|
+### 2. `book-recovery-scanner.ts` — book 级别自愈扫描器
|
|
|
+
|
|
|
+- 每 60s 扫一次
|
|
|
+- 触发条件(任一):
|
|
|
+ - `outline_ready` 超 30min 未动
|
|
|
+ - `content_generating` 超 60min 无章节推进
|
|
|
+ - **书标记已完成但仍有章节漏内容**(新增场景 3)
|
|
|
+- **不再触发 LangGraph 全量重跑**,改用并发单节补全(`fillMissingChapters`,3 worker pool)
|
|
|
+- 每轮最多 3 本书,避免 LLM 配额爆
|
|
|
+- 失败回写 `failed`,不出现永远卡住的状态
|
|
|
+
|
|
|
+### 3. `audio-scanner.ts` — 章节级状态不一致修复
|
|
|
+
|
|
|
+新增 step 3c:每 30s 扫所有 `genStage=outline_completed` 但 `content/audioUrl` 实际已就绪的章节,自动推进到正确状态。
|
|
|
+
|
|
|
+### 4. `langgraph-controller.ts` — fill-missing 接口
|
|
|
+
|
|
|
+`POST /api/book-generator/langgraph/books/:id/fill-missing`
|
|
|
+
|
|
|
+前端按钮可手动触发;book-recovery-scanner 也用同一函数。找不到缺失章节返回 `missingCount=0`。
|
|
|
+
|
|
|
+### 5. `detail.vue` — 前端兜底
|
|
|
+
|
|
|
+- `BookGenStage` 枚举补 `outline_generating / outline_ready / outline_failed / generating_content`
|
|
|
+- `outline_ready` 状态显示橙色 "📋 大纲已生成" + 🚀 按钮调 `/interactive/generate`
|
|
|
+- `stalledTip` 块加 "⚡ 补全缺失章节" 按钮
|
|
|
+- **始终可用的兜底块**:任何时候只要检测到章节内容缺失(`hasMissingChapters`)就显示蓝色 "🩹 检测到 N 个章节未生成内容" + 调 `/fill-missing`
|
|
|
+- `outlineReadyPolling` 30s 降频轮询,检测到后端自愈推进自动切回正常轮询
|
|
|
+
|
|
|
+### 6. `poll.ts` — 测试超时放宽
|
|
|
+
|
|
|
+180s → 300s(生产 LangGraph 跑 1000 字实测 3 分 48 秒 + 1 分钟余量)。
|
|
|
+
|
|
|
+## 验证证据
|
|
|
+
|
|
|
+| 场景 | 实测 |
|
|
|
+|---|---|
|
|
|
+| book 43 真实状态 | `audio_completed` + 100% + 72/25 章 + 0 缺失 |
|
|
|
+| fill-missing 接口 | `{"code":0,"message":"书籍已完整","missingCount":0}` |
|
|
|
+| rich_outline 强压力测试 | LLM mock 必失败 → 兜底3 成功,outlineJson 必落库 |
|
|
|
+| 新建书 1000 字 LangGraph 全链路 | 3 分 48 秒从创建到 `content_completed` + 100% |
|
|
|
+
|
|
|
+## 端到端演示命令
|
|
|
+
|
|
|
+```bash
|
|
|
+# 1. 登录拿 token
|
|
|
+curl -X POST https://bookapi.rrbrr.com/api/auth/login \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -d '{"phone":"13xxx","code":"123456"}'
|
|
|
+
|
|
|
+# 2. 创建书触发 fast-path
|
|
|
+curl -X POST https://bookapi.rrbrr.com/api/book-generator/langgraph/books \
|
|
|
+ -H "Authorization: Bearer <token>" \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -d '{"title":"测试","description":"...","bookScale":"1000","autoGenerateContent":true,"autoGenerateAudio":true}'
|
|
|
+
|
|
|
+# 3. 手动补全缺失章节(任何时候)
|
|
|
+curl -X POST https://bookapi.rrbrr.com/api/book-generator/langgraph/books/<id>/fill-missing \
|
|
|
+ -H "Authorization: Bearer <token>"
|
|
|
+```
|
|
|
+
|
|
|
+## 教训
|
|
|
+
|
|
|
+1. **后端兜底不能依赖 LangGraph**:LangGraph 有超时(rich_outline 720s),超时后默认 catch 块只标 failed,会触发循环。**自愈路径必须用单节补全,不要重跑全量**。
|
|
|
+2. **前端必须识别所有后端状态值**:`genStage` 枚举要包含 `outline_ready` 等 intermediate 值,否则用户看到的永远是"中间状态"。
|
|
|
+3. **章节状态是真相,book 字段只是 hint**:`computeBookGenStage` 用 chapter 推断时,要确保 BookChapter 状态被同步推进;否则写一个 `audio-scanner` 兜底修复。
|
|
|
+4. **测试超时反映真实环境**:180s 在生产不够,要按 99 分位数 + 1 分钟余量设。
|
|
|
+5. **关键接口必须给前端兜底入口**:fill-missing 接口 + 按钮,让用户在自动兜底失败时能手动触发,避免"卡死无人能救"。
|
|
|
+
|
|
|
+## 监控建议(待做)
|
|
|
+
|
|
|
+- 定时检查"任何书 genStage 在中间态超过 2 小时"主动报警(钉钉/邮件)
|
|
|
+- book-recovery-scanner 每次扫描输出统计摘要
|
|
|
+- 给 rich_outline 节点的 fallback 触发次数加 metric
|