All files / modules/book-generator book-queue.processor.ts

0% Statements 0/48
0% Branches 0/1
0% Functions 0/1
0% Lines 0/48

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85                                                                                                                                                                         
/**
 * 书籍生成 - 数据库驱动(替代队列)
 *
 * 之前使用 Bull/Redis + 内存队列,但队列在开发环境下不可靠。
 * 改为完全靠数据库驱动:
 *   1. genStage 字段直接追踪进度
 *   2. 服务启动时自动恢复中断的任务
 *   3. 生成异步执行,不阻塞请求
 */
 
import { bookStore } from './book-generator.store';
import { prisma } from '../../models';
import { memoryQueue } from '../../services/memory-queue';
 
/**
 * 服务启动时恢复中断的生成任务
 *
 * 原则:
 *  1. 已有内容产出的书籍不重新生成(保护已有成果)
 *  2. 只恢复真正中断在早期阶段的书籍(无大纲/无内容)
 *  3. 音频遗漏由 AudioScanner 兜底,不需要重跑整个生成流程
 */
export async function resumeInterruptedTasks() {
  console.log('[DBQueue] 扫描中断的生成任务...');
 
  try {
    // 只恢复早期阶段:outlining(无大纲)、outline_completed(有大纲但无内容开始)
    // 跳过 content_completed / audio_generating / audio_completed / video_generating
    // 这些阶段的书籍已有产出,音频遗漏由 AudioScanner 兜底
    const earlyStages = ['outlining', 'outline_completed'];
 
    const interruptedBooks = await prisma.book.findMany({
      where: { genStage: { in: earlyStages } },
      select: { id: true, title: true, description: true, bookScale: true, genStage: true },
    });
 
    if (interruptedBooks.length === 0) {
      console.log('[DBQueue] 没有需要恢复的早期中断任务');
      return;
    }
 
    // 进一步过滤:如果书籍已有章节(说明之前生成过部分内容),也跳过
    const booksToResume: typeof interruptedBooks = [];
    for (const book of interruptedBooks) {
      const chapterCount = await prisma.bookChapter.count({ where: { bookId: book.id } });
      if (chapterCount === 0) {
        booksToResume.push(book);
        console.log(`[DBQueue] 需恢复: 《${book.title}》 (bookId=${book.id}, stage=${book.genStage}, 章节数=0)`);
      } else {
        console.log(`[DBQueue] ⏭️ 跳过: 《${book.title}》 (bookId=${book.id}, stage=${book.genStage}, 已有${chapterCount}个章节,不破坏已有成果)`);
      }
    }
 
    if (booksToResume.length === 0) {
      console.log('[DBQueue] 所有早期中断书籍都已有章节产出,跳过恢复(音频由 AudioScanner 兜底)');
      return;
    }
 
    console.log(`[DBQueue] 恢复 ${booksToResume.length} 个真正中断的任务...`);
 
    const { langGraphGenerator } = await import('./index.js');
    for (const book of booksToResume) {
      console.log(`[DBQueue] 恢复: 《${book.title}》 (bookId=${book.id})`);
      langGraphGenerator.generate(
        book.id.toString(),
        book.description || book.title,
        book.bookScale || '1000'
      ).then(() => {
        console.log(`[DBQueue] 恢复完成: bookId=${book.id}`);
      }).catch(err => {
        console.error(`[DBQueue] 恢复失败: bookId=${book.id}`, err);
      });
    }
  } catch (error) {
    console.error('[DBQueue] 扫描恢复失败:', error);
  }
}
 
/**
 * 初始化(保持兼容,不再需要队列处理器注册)
 */
export function initBookGenerationQueue() {
  console.log('[DBQueue] 已启用数据库驱动模式(跳过队列)');
}