# All files / modules/book-generator stage-manager.ts

97.14% Statements 102/105
84.21% Branches 16/19
100% Functions 5/5
97.14% Lines 102/105

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
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    1771x

    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x

    1x
    67x
    67x

    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x

    1x
    16x
    16x
    16x
    16x
    16x

    16x
    16x
    13x
    13x
    13x
    16x
    15x
    15x
    15x
    16x
    16x

    14x
    14x
    14x
    14x
    14x

    14x
    14x
    2x
    2x
    2x
    2x
    2x

    12x

    12x
    12x
    12x
    12x

    14x

    3x
    3x
    3x
    3x
    3x

    3x
    2x
    2x
    2x

    3x
    1x
    1x
    1x

    14x

    5x
    5x
    5x
    5x
    5x
    5x
    5x

    5x
    5x
    5x

    5x

    2x
    2x

    2x
    2x

    3x
    3x

    5x
    5x
    5x
    5x
    5x
    5x
    5x

    5x
    5x
    4x

     /**
     * 状态系统 - 阶段管理器
     * 线性阶段模型：统一的状态字段 + 阶段索引比较
     */

    import { prisma } from '../../models';
    import { ChapterGenStage, BookGenStage } from './book-generator.types';

    // 重新导出通用类型（统一从 book-generator.types.ts 获取定义）
    export type { ChapterGenStage, BookGenStage };

    // ============ 阶段索引（用于比较大小） ============

    const CHAPTER_STAGE_ORDER: ChapterGenStage[] = [
      'idle',                // 0
      'outline_completed',   // 1
      'content_generating',  // 2
      'content_completed',   // 3
      'audio_generating',    // 4
      'audio_completed',     // 5
      'video_generating',    // 6
      'video_completed',     // 7
      'failed',              // 8（终态）
    ];

    export function chapterStageIndex(s: ChapterGenStage): number {
      return CHAPTER_STAGE_ORDER.indexOf(s);
    }

    // ============ 章节转移矩阵 ============

    const CHAPTER_VALID_TRANSITIONS: Record<ChapterGenStage, ChapterGenStage[]> = {
      'idle': ['content_generating', 'failed'],
      'outline_completed': ['content_generating', 'failed'],
      'content_generating': ['content_completed', 'failed'],
      'content_completed': ['audio_generating', 'audio_completed', 'content_generating', 'failed'],
      'audio_generating': ['audio_completed', 'content_completed', 'failed'],  // 允许音频失败回退到 content_completed
      'audio_completed': ['video_generating', 'audio_generating', 'content_generating', 'failed'],
      'video_generating': ['video_completed', 'failed'],
      'video_completed': ['video_generating', 'audio_generating', 'content_generating', 'failed'],
      'failed': ['content_generating', 'audio_generating', 'video_generating', 'idle'],
    };

    // ============ 资源清理规则 ============

    /**
     * 判断本次章节转移是否需要清理下游资源。
     * 如果目标阶段比某资源的阶段索引更小，则清除该资源。
     */
    export function getCleanupForChapterRegression(
      targetStage: ChapterGenStage,
    ): { audioUrl?: null; audioDuration?: number; videoUrl?: null; videoDuration?: number | null } {
      const targetIdx = chapterStageIndex(targetStage);
      const audioStageIdx = chapterStageIndex('audio_completed');    // 4
      const videoStageIdx = chapterStageIndex('video_completed');    // 6

      const clean: any = {};
      if (targetIdx < audioStageIdx) {
        clean.audioUrl = null;
        clean.audioDuration = 0;
      }
      if (targetIdx < videoStageIdx) {
        clean.videoUrl = null;
        clean.videoDuration = null;
      }
      return clean;
    }

    // ============ 安全转移函数 ============

    /**
     * 安全转移章节状态
     * 验证转移矩阵 + 乐观锁 + 资源清理
     */
    export async function safeTransitionChapter(
      chapterId: number,
      currentStage: ChapterGenStage,
      targetStage: ChapterGenStage,
    ): Promise<void> {
      // ① 验证是否在转移矩阵中
      const allowed = CHAPTER_VALID_TRANSITIONS[currentStage];
      if (!allowed || !allowed.includes(targetStage)) {
        throw new Error(
          `非法状态转移：Chapter ${chapterId} 当前 ${currentStage}，` +
          `不允许转到 ${targetStage}。允许的转移：[${allowed?.join(', ') || '无'}]`
        );
      }

      // ② 判断是否需要清理下游资源
      const cleanData = getCleanupForChapterRegression(targetStage);

      // ③ 乐观锁写入
      const result = await prisma.bookChapter.updateMany({
        where: { id: chapterId, genStage: currentStage },
        data: { genStage: targetStage, ...cleanData },
      });

      if (result.count === 0) {
        // 如果状态已经被其他操作改变，查询当前实际状态
        const currentChapter = await prisma.bookChapter.findUnique({
          where: { id: chapterId },
          select: { genStage: true }
        });
        const actualStage = currentChapter?.genStage || 'unknown';

        // 如果目标阶段和实际阶段相同，说明已经转移过了，这不是错误
        if (actualStage === targetStage) {
          console.log(`[safeTransition] 章节 ${chapterId} 已处于 ${targetStage}，跳过`);
          return;
        }

        // 如果是回退到 failed 状态（可能之前已经失败了），也跳过
        if (currentStage === 'failed' && targetStage !== 'idle') {
          console.log(`[safeTransition] 章节 ${chapterId} 已在 failed 状态，跳过转移到 ${targetStage}`);
          return;
        }

        // 其他情况：记录警告但不崩溃
        console.warn(`[safeTransition] 章节 ${chapterId} 状态冲突：预期 ${currentStage}，实际 ${actualStage}，目标 ${targetStage}`);
        return;
      }
    }

    /**
     * 安全转移书籍状态
     */
    // ============ 上层封装 ============

    /**
     * 前进：推进到下一个阶段。只能前进不能回退。
     * 例：content_completed → audio_generating
     */
    export async function advanceChapter(
      chapterId: number,
      targetStage: ChapterGenStage,
    ): Promise<void> {
      const chapter = await prisma.bookChapter.findUniqueOrThrow({
        where: { id: chapterId },
      });

      const cur = chapter.genStage as ChapterGenStage;
      const curIdx = chapterStageIndex(cur);
      const tgtIdx = chapterStageIndex(targetStage);

      if (tgtIdx <= curIdx) {
        // 不允许回退或同级跳转（由 regenerateChapter 处理）
        throw new Error(
          `advanceChapter 不能用于回退：当前 ${cur}，目标 ${targetStage}。` +
          `请使用 regenerateChapter()`
        );
      }

      await safeTransitionChapter(chapterId, cur, targetStage);
    }

    /**
     * 重新生成：回退到上游阶段（或同级 generating 状态）以重新生成。
     * 自动清除下游资源。
     * 例：audio_completed → audio_generating（重新生成音频）
     *     audio_completed → content_generating（从内容重新开始）
     *     failed → audio_generating（失败重试）
     */
    export async function regenerateChapter(
      chapterId: number,
      targetStage: ChapterGenStage,
    ): Promise<void> {
      const chapter = await prisma.bookChapter.findUniqueOrThrow({
        where: { id: chapterId },
      });

      const cur = chapter.genStage as ChapterGenStage;
      await safeTransitionChapter(chapterId, cur, targetStage);
    }

    // ============ Book 联动回退逻辑 ============