/** * 一次性数据库修复脚本:清理被误标为卡死/失败的书籍 * * 背景: * - 之前 quality-check / continuity-edit / rewrite 等节点把"成功报告"写到了 Book.errorMsg * 字段,前端误以为生成失败 * - 同时有 17+ 本书 genStage=audio_generating 但章节 audioUrl 已就绪,是 audioScanner * 未及时自愈的卡死状态 * * 本脚本做三件事(全部幂等,可重跑): * 1. 把 audio_generating + 章节 audioUrl 已设 的书的 Book.genStage 推进到 audio_completed * 2. 把 Book.errorMsg 中符合 [质量校验] / [连贯性编辑] / [改写优化] / [改写降级] 正则的清空 * 3. 输出修复前后对比,方便回滚 * * 用法: * cd server && npx tsx scripts/fix-stuck-books.ts * npx tsx scripts/fix-stuck-books.ts --dry-run # 只统计不写 */ import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); const REPORT_LIKE_PREFIXES = [ '[质量校验]', '[连贯性编辑]', '[改写优化]', '[改写降级]', ]; function isReportLike(msg: string | null | undefined): boolean { if (!msg) return false; return REPORT_LIKE_PREFIXES.some((p) => msg.startsWith(p)); } async function main() { const dryRun = process.argv.includes('--dry-run'); console.log(`[fix-stuck-books] 启动${dryRun ? ' (DRY-RUN, 不写库)' : ''}`); // === 1. 找出 audio_generating + 章节 audioUrl 已设的书 === const stuckBooks = await prisma.book.findMany({ where: { genStage: 'audio_generating', chapters: { some: { audioUrl: { not: '' }, }, }, }, select: { id: true, title: true, genStage: true, progress: true, errorMsg: true, chapters: { where: { audioUrl: { not: '' } }, select: { id: true }, }, }, take: 100, }); console.log(`\n[1] 卡死书籍(audio_generating 且有音频 URL): ${stuckBooks.length} 本`); for (const b of stuckBooks) { console.log(` id=${b.id} 《${b.title}》 chapters=${b.chapters.length} err=${(b.errorMsg || '').substring(0, 50)}`); } if (!dryRun && stuckBooks.length > 0) { for (const b of stuckBooks) { await prisma.book.update({ where: { id: b.id }, data: { genStage: 'audio_completed', progress: 100 }, }); console.log(` ✅ id=${b.id} → audio_completed`); } } // === 2. 清空 errorMsg 中的"报告式"内容 === const pollutedBooks = await prisma.book.findMany({ where: { errorMsg: { not: null }, }, select: { id: true, title: true, errorMsg: true, genStage: true, failedStage: true }, take: 500, }); const toClean = pollutedBooks.filter((b) => isReportLike(b.errorMsg)); console.log(`\n[2] errorMsg 误写为报告的书: ${toClean.length} 本`); for (const b of toClean) { console.log(` id=${b.id} 《${b.title}》 stage=${b.genStage} failed=${b.failedStage ?? '-'} err=${(b.errorMsg || '').substring(0, 60)}`); } if (!dryRun && toClean.length > 0) { for (const b of toClean) { // 只清 errorMsg,保留其他字段不动 await prisma.book.update({ where: { id: b.id }, data: { errorMsg: null }, }); console.log(` ✅ id=${b.id} errorMsg 已清空`); } } // === 3. 输出对比 === console.log(`\n[3] 修复总结:`); console.log(` 卡死书籍推进: ${stuckBooks.length} 本 → audio_completed`); console.log(` 误报清空: ${toClean.length} 本`); console.log(` 数据库总改动: ${stuckBooks.length + toClean.length} 条`); console.log(`\n[fix-stuck-books] 完成`); } main() .catch((e) => { console.error('[fix-stuck-books] 异常:', e); process.exitCode = 1; }) .finally(async () => { await prisma.$disconnect(); });