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