# All files / modules/book-generator/nodes outline.node.ts

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

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

     /**
     * 大纲生成节点（集成容错机制）
     */

    import { GraphState } from '../graph';
    import { bookStore } from '../book-generator.store';
    import { callLLMWithMessages } from '../../../services/llm';
    import { parseOutline } from '../parsers/outline.parser';
    import { buildOutlineMessages, SCALE_CHAPTER_RANGE } from '../prompts/builder';
    import { PROGRESS } from '../utils';
    import { callLLMWithRetry, executeNodeWithTimeout, FAULT_TOLERANCE_CONFIG } from '../fault-tolerance';
    import { getChapterRange } from '../book-type-config';

    export async function generateOutlineNode(state: typeof GraphState.State): Promise<Partial<typeof GraphState.State>> {
      console.log('[LangGraph] 生成大纲, bookId:', state.bookId, 'scale:', state.bookScale);

      // 读取前序规划节点的分析结果，用于指导大纲生成
      let bookPlan: any = null;
      if (state.bookPlan) {
        try {
          bookPlan = JSON.parse(state.bookPlan);
          console.log('[LangGraph] 使用规划结果指导大纲:', bookPlan.structureLogic, bookPlan.writingStyle);
        } catch {
          console.warn('[LangGraph] bookPlan 解析失败，忽略');
        }
      }

      try {
        // 使用容错包装器执行AI调用
        const response = await executeNodeWithTimeout(
          state.bookId,
          'generate_outline',
          async () => {
            return callLLMWithRetry(
              buildOutlineMessages(state.topic, state.bookScale, state.description, bookPlan),
              undefined,
              {
                bookId: state.bookId,
                nodeId: 'generate_outline',
                attempt: 0,
                maxAttempts: FAULT_TOLERANCE_CONFIG.aiRetry.maxRetries,
              }
            );
          },
          FAULT_TOLERANCE_CONFIG.nodeTimeout.generate_outline
        );

        const outline = parseOutline(response);
        if (!outline) throw new Error('大纲解析失败');

        // 校验章节数：允许 ±20% 浮动，超出才截断/补充
        const targetChapters = SCALE_CHAPTER_RANGE[state.bookScale as keyof typeof SCALE_CHAPTER_RANGE];
        if (targetChapters) {
          const { min: minAllowed, max: maxAllowed } = getChapterRange(targetChapters);
          const actual = outline.chapters.length;

          if (actual > maxAllowed) {
            console.warn(`[LangGraph] AI 返回章节数 ${actual} 超出上限 ${maxAllowed}，截断`);
            outline.chapters = outline.chapters.slice(0, maxAllowed);
          } else if (actual < minAllowed) {
            console.warn(`[LangGraph] AI 返回章节数 ${actual} 少于下限 ${minAllowed}，使用话题相关的默认章节补充`);
            const topicHint = state.topic?.substring(0, 30) || '';
            const perChapterWords = Math.round(parseInt(state.bookScale) / minAllowed);
            const gapTopics = [
              `概述与背景`,
              `核心概念`,
              `深入探究`,
              `应用与实践`,
              `总结与展望`,
            ];
            while (outline.chapters.length < minAllowed) {
              const idx = outline.chapters.length;
              const gapTitle = gapTopics[idx % gapTopics.length];
              outline.chapters.push({
                number: idx + 1,
                title: topicHint ? `第${idx + 1}章 ${topicHint} - ${gapTitle}` : `第${idx + 1}章 ${gapTitle}`,
                summary: `本章围绕"${topicHint || gapTitle}"展开，介绍${gapTitle}相关内容。`,
                keyPoints: [gapTitle, '关键知识点', '实践要点'],
                estimatedWords: perChapterWords,
              });
            }
          }
          // 在范围内（±20%）不做任何处理，让 AI 自己决定
        }

        // 短文类至少1个章节
        if (outline.chapters.length === 0) {
          console.warn('[LangGraph] AI 返回章节数为0，使用话题创建默认章节');
          const fallbackTitle = state.topic?.substring(0, 50) || '正文';
          outline.chapters = [{
            number: 1,
            title: fallbackTitle,
            summary: `关于"${fallbackTitle}"的全面介绍，涵盖核心概念、关键知识和实践要点。`,
            keyPoints: ['核心概念', '关键知识', '实践要点'],
            estimatedWords: parseInt(state.bookScale) || 800,
          }];
        }

        const totalChapters = outline.chapters.length;
        await bookStore.update(state.bookId, {
          totalChapters,
          outlineJson: JSON.stringify(outline),
          progress: PROGRESS.OUTLINE_DONE
        });
        await bookStore.createChapters(state.bookId, outline.chapters.map(c => ({
          number: c.number,
          title: c.title,
          summary: c.summary,
          keyPoints: c.keyPoints,
          estimatedWords: c.estimatedWords,
        })));

        console.log('[LangGraph] 大纲生成完成，章节数:', outline.chapters.length);
        return { progress: PROGRESS.OUTLINE_DONE };
      } catch (error) {
        console.error('[LangGraph] 大纲生成失败:', error);
        // 更新书籍状态为失败
        await bookStore.update(state.bookId, {
          genStage: 'failed',
          failedStage: 'outlining',
          errorMsg: error instanceof Error ? error.message : '大纲生成失败',
        });
        return {
          error: error instanceof Error ? error.message : '大纲生成失败',
          finished: true
        };
      }
    }