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

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

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
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220

     /**
     * 书籍规划节点（planBookNode）
     * 在生成任何内容之前，由 AI 像职业作家一样对整本书做详细规划：
     *   - 分析书籍类型、目标读者、内容深度
     *   - 决定大纲层级（1/2/3层）
     *   - 规划写作风格和结构逻辑
     *   - 输出完整的写作方案
     *
     * 该节点的输出（planResult）会被存储到 GraphState，供后续节点参考。
     */

    import { GraphState } from '../graph';
    import { bookStore } from '../book-generator.store';
    import { callLLMWithMessages, ChatMessage } from '../../../services/llm';
    import { PROGRESS } from '../utils';
    import { getScaleConfig } from '../book-type-config';

    /**
     * 书籍规划输出接口
     */
    export interface BookPlan {
      /** 最终确定的大纲层级 */
      genLevel: number;
      /** 书籍类型分析 */
      bookTypeAnalysis: string;
      /** 推荐的写作风格 */
      writingStyle: string;
      /** 结构逻辑说明 */
      structureLogic: string;
      /** 内容深度评估 */
      contentDepth: string;
      /** 目标读者分析 */
      targetAudienceAnalysis: string;
      /** 规划理由 */
      reasoning: string;
    }

    /**
     * 从描述中提取明确的字数要求
     * 如 "生成200字文本" → 200
     */
    function extractWordCountFromDescription(description: string): number | null {
      const text = description.toLowerCase();
      // 匹配 "200字"、"500字" 等
      const match = text.match(/(\d+)\s*字/i);
      if (match) {
        const n = parseInt(match[1]);
        return isNaN(n) ? null : n;
      }
      return null;
    }

    /**
     * 构建规划提示词
     */
    function buildPlanPrompt(
      title: string,
      description: string,
      bookScale: string
    ): string {
      const config = getScaleConfig(bookScale);
      const chapters = config?.chapters || 17;

      // 如果 description 里有明确字数要求，优先用它
      const descWordCount = extractWordCountFromDescription(description);
      const totalWords = descWordCount !== null
        ? descWordCount
        : (config?.totalWords || 130000);

      return `你是一位经验丰富的图书策划编辑。请对以下书籍做全面规划。

    ## 书籍信息
    - 书名：${title}
    - 字数规模：约${totalWords}字，约${chapters}章
    - 描述：${description}

    ## 你的职责
    像职业作家出书前一样，先对整本书做完整的规划分析。

    ## 分析维度

    ### 1. 内容类型分析
    判断这本书属于以下哪种类型：
    - 教材/学术类：系统教学、理论知识体系完整
    - 技术教程类：有操作步骤、代码、实践指南
    - 小说/文学类：叙事性、故事性内容
    - 商业/经管类：管理、营销、投资、创业
    - 科普/大众类：普及科学知识，通俗易懂
    - 其他类型

    ### 2. 大纲层级决策
    根据内容类型和字数规模，决定使用几层大纲：
    - 1层（仅章）：适合小说、短文、浅层内容。每章直接展开写，无需拆分。
    - 2层（章→节）：适合商业、科普、一般教程。每章拆 2-4 节，节内直接写内容。
    - 3层（章→节→小节）：适合教材、技术书、系统教学。每章拆节，节下再拆小节。

    ### 3. 写作方案
    - 目标读者是谁？他们需要什么？
    - 用什么写作风格？（通俗易懂/专业严谨/轻松幽默/故事化）
    - 内容如何组织递进？（由浅入深、由理论到实践、问题驱动等）

    ## 输出格式

    你必须严格按照以下 JSON 格式返回，不要包含任何其他文字：

    {
      "genLevel": 1,
      "bookType": "教材|技术教程|小说|商业|科普",
      "bookTypeAnalysis": "对书籍类型的详细分析，2-3句话",
      "writingStyle": "推荐的写作风格",
      "structureLogic": "内容组织逻辑，如'由浅入深'、'由理论到实践'",
      "contentDepth": "内容深度评估（入门/基础/进阶/高级/专家）",
      "targetAudienceAnalysis": "目标读者分析，1-2句话",
      "reasoning": "为什么选择这个大纲层级？1-2句话"
    }`;
    }

    /**
     * 解析 AI 返回的规划 JSON
     */
    function parsePlanResponse(response: string): BookPlan | null {
      try {
        let cleaned = response.trim();

        // 移除 think 标签
        const thinkEnd = cleaned.indexOf('</think>');
        if (thinkEnd !== -1) {
          cleaned = cleaned.substring(thinkEnd + 8).trim();
        }

        // 提取 JSON
        const match = cleaned.match(/\{[\s\S]*\}/);
        if (!match) {
          console.error('[PlanNode] 未找到 JSON');
          return null;
        }

        const data = JSON.parse(match[0]);

        // 验证必要字段
        if (typeof data.genLevel !== 'number' || ![1, 2, 3].includes(data.genLevel)) {
          console.error('[PlanNode] genLevel 无效:', data.genLevel);
          return null;
        }

        return {
          genLevel: data.genLevel,
          bookTypeAnalysis: data.bookTypeAnalysis || '',
          writingStyle: data.writingStyle || '',
          structureLogic: data.structureLogic || '',
          contentDepth: data.contentDepth || '',
          targetAudienceAnalysis: data.targetAudienceAnalysis || '',
          reasoning: data.reasoning || '',
        };
      } catch (err) {
        console.error('[PlanNode] 解析失败:', err);
        return null;
      }
    }

    /**
     * 书籍规划节点
     * 作为 LangGraph 的第一个节点运行
     */
    export async function planBookNode(state: typeof GraphState.State): Promise<Partial<typeof GraphState.State>> {
      console.log('[PlanNode] 开始规划书籍, bookId:', state.bookId);

      // 用户明确指定了 genLevel → AI 规划可以覆盖风格等，但层级必须保留
      const userDefinedLevel = state.userSpecifiedGenLevel !== undefined;

      try {
        const book = await bookStore.getById(state.bookId);
        const title = book?.title || state.topic;
        const description = book?.description || '';
        const bookScale = state.bookScale || '1000';

        const prompt = buildPlanPrompt(title, description, bookScale);

        const messages: ChatMessage[] = [
          { role: 'system', content: '你是一位资深的图书策划编辑。请严格按照要求的 JSON 格式输出规划结果，不要添加任何额外文字。' },
          { role: 'user', content: prompt },
        ];

        const response = await callLLMWithMessages(messages);
        const plan = parsePlanResponse(response);

        if (!plan) {
          console.warn('[PlanNode] AI 规划解析失败，使用默认值');
          return { progress: PROGRESS.OUTLINE_DONE };
        }

        console.log(`[PlanNode] 规划完成: genLevel=${plan.genLevel}, 类型=${(response.match(/"bookType"\s*:\s*"([^"]+)"/) || [])[1] || '未知'}, 风格=${plan.writingStyle}`);

        // 用户明确选了层级（含1/2/3）→ 保留用户选择；否则 → AI 决定
        const finalGenLevel = userDefinedLevel
          ? (state.userSpecifiedGenLevel ?? state.genLevel) // 用户主动选择，保留
          : plan.genLevel;                                    // 自动模式，AI决定

        console.log(`[PlanNode] genLevel: ${state.genLevel} → ${finalGenLevel} (userSpecified=${state.userSpecifiedGenLevel}), 风格=${plan.writingStyle}, 结构=${plan.structureLogic}`);

        // 持久化到数据库 + 通过 GraphState 传递给后续节点
        await bookStore.update(state.bookId, {
          bookAnalysis: JSON.stringify(plan),
        });

        return {
          progress: PROGRESS.OUTLINE_DONE,
          genLevel: finalGenLevel,
          bookPlan: JSON.stringify(plan),
        };
      } catch (error) {
        console.error('[PlanNode] 规划失败:', error);
        // 不阻塞流程，继续使用默认 genLevel
        await bookStore.update(state.bookId, {
          progress: PROGRESS.OUTLINE_DONE,
        });
        return { progress: PROGRESS.OUTLINE_DONE };
      }
    }