book-43-stuck-fix.md 5.7 KB

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_completedcomputeBookGenStage 推断前端还是"大纲阶段"
  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_completedcontent/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%

端到端演示命令

# 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 字段只是 hintcomputeBookGenStage 用 chapter 推断时,要确保 BookChapter 状态被同步推进;否则写一个 audio-scanner 兜底修复。
  4. 测试超时反映真实环境:180s 在生产不够,要按 99 分位数 + 1 分钟余量设。
  5. 关键接口必须给前端兜底入口:fill-missing 接口 + 按钮,让用户在自动兜底失败时能手动触发,避免"卡死无人能救"。

监控建议(待做)

  • 定时检查"任何书 genStage 在中间态超过 2 小时"主动报警(钉钉/邮件)
  • book-recovery-scanner 每次扫描输出统计摘要
  • 给 rich_outline 节点的 fallback 触发次数加 metric