# 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] 已启用数据库驱动模式（跳过队列）');
    }