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
    };
  }
}