fix-stuck-books.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. /**
  2. * 一次性数据库修复脚本:清理被误标为卡死/失败的书籍
  3. *
  4. * 背景:
  5. * - 之前 quality-check / continuity-edit / rewrite 等节点把"成功报告"写到了 Book.errorMsg
  6. * 字段,前端误以为生成失败
  7. * - 同时有 17+ 本书 genStage=audio_generating 但章节 audioUrl 已就绪,是 audioScanner
  8. * 未及时自愈的卡死状态
  9. *
  10. * 本脚本做三件事(全部幂等,可重跑):
  11. * 1. 把 audio_generating + 章节 audioUrl 已设 的书的 Book.genStage 推进到 audio_completed
  12. * 2. 把 Book.errorMsg 中符合 [质量校验] / [连贯性编辑] / [改写优化] / [改写降级] 正则的清空
  13. * 3. 输出修复前后对比,方便回滚
  14. *
  15. * 用法:
  16. * cd server && npx tsx scripts/fix-stuck-books.ts
  17. * npx tsx scripts/fix-stuck-books.ts --dry-run # 只统计不写
  18. */
  19. import { PrismaClient } from '@prisma/client';
  20. const prisma = new PrismaClient();
  21. const REPORT_LIKE_PREFIXES = [
  22. '[质量校验]',
  23. '[连贯性编辑]',
  24. '[改写优化]',
  25. '[改写降级]',
  26. ];
  27. function isReportLike(msg: string | null | undefined): boolean {
  28. if (!msg) return false;
  29. return REPORT_LIKE_PREFIXES.some((p) => msg.startsWith(p));
  30. }
  31. async function main() {
  32. const dryRun = process.argv.includes('--dry-run');
  33. console.log(`[fix-stuck-books] 启动${dryRun ? ' (DRY-RUN, 不写库)' : ''}`);
  34. // === 1. 找出 audio_generating + 章节 audioUrl 已设的书 ===
  35. const stuckBooks = await prisma.book.findMany({
  36. where: {
  37. genStage: 'audio_generating',
  38. chapters: {
  39. some: {
  40. audioUrl: { not: '' },
  41. },
  42. },
  43. },
  44. select: {
  45. id: true,
  46. title: true,
  47. genStage: true,
  48. progress: true,
  49. errorMsg: true,
  50. chapters: {
  51. where: { audioUrl: { not: '' } },
  52. select: { id: true },
  53. },
  54. },
  55. take: 100,
  56. });
  57. console.log(`\n[1] 卡死书籍(audio_generating 且有音频 URL): ${stuckBooks.length} 本`);
  58. for (const b of stuckBooks) {
  59. console.log(` id=${b.id} 《${b.title}》 chapters=${b.chapters.length} err=${(b.errorMsg || '').substring(0, 50)}`);
  60. }
  61. if (!dryRun && stuckBooks.length > 0) {
  62. for (const b of stuckBooks) {
  63. await prisma.book.update({
  64. where: { id: b.id },
  65. data: { genStage: 'audio_completed', progress: 100 },
  66. });
  67. console.log(` ✅ id=${b.id} → audio_completed`);
  68. }
  69. }
  70. // === 2. 清空 errorMsg 中的"报告式"内容 ===
  71. const pollutedBooks = await prisma.book.findMany({
  72. where: {
  73. errorMsg: { not: null },
  74. },
  75. select: { id: true, title: true, errorMsg: true, genStage: true, failedStage: true },
  76. take: 500,
  77. });
  78. const toClean = pollutedBooks.filter((b) => isReportLike(b.errorMsg));
  79. console.log(`\n[2] errorMsg 误写为报告的书: ${toClean.length} 本`);
  80. for (const b of toClean) {
  81. console.log(` id=${b.id} 《${b.title}》 stage=${b.genStage} failed=${b.failedStage ?? '-'} err=${(b.errorMsg || '').substring(0, 60)}`);
  82. }
  83. if (!dryRun && toClean.length > 0) {
  84. for (const b of toClean) {
  85. // 只清 errorMsg,保留其他字段不动
  86. await prisma.book.update({
  87. where: { id: b.id },
  88. data: { errorMsg: null },
  89. });
  90. console.log(` ✅ id=${b.id} errorMsg 已清空`);
  91. }
  92. }
  93. // === 3. 输出对比 ===
  94. console.log(`\n[3] 修复总结:`);
  95. console.log(` 卡死书籍推进: ${stuckBooks.length} 本 → audio_completed`);
  96. console.log(` 误报清空: ${toClean.length} 本`);
  97. console.log(` 数据库总改动: ${stuckBooks.length + toClean.length} 条`);
  98. console.log(`\n[fix-stuck-books] 完成`);
  99. }
  100. main()
  101. .catch((e) => {
  102. console.error('[fix-stuck-books] 异常:', e);
  103. process.exitCode = 1;
  104. })
  105. .finally(async () => {
  106. await prisma.$disconnect();
  107. });