| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- /**
- * 一次性数据库修复脚本:清理被误标为卡死/失败的书籍
- *
- * 背景:
- * - 之前 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();
- });
|