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

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

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

     /**
     * 深度规划节点（deepPlanBookNode）
     *
     * 用 LLM 替代简单正则做深度需求分析和书籍规划。
     * 输出 goldenThread、narrativeArc、crossReferences、toneProfile、audienceCalibration，
     * 同时兼容原 planBookNode 的 genLevel、writingStyle 等字段。
     *
     * 解决 issue #3（需求分析用正则不是AI）
     */

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

    /**
     * 深度规划输出接口（扩展原 BookPlan）
     */
    export interface DeepBookPlan {
      genLevel: number;
      bookType: string;
      bookTypeAnalysis: string;
      writingStyle: string;
      structureLogic: string;
      contentDepth: string;
      targetAudienceAnalysis: string;
      reasoning: string;
      goldenThread: string;
      narrativeArc: Record<string, { theme: string; chapters: number[]; goal: string }>;
      crossReferences: Record<string, { dependsOn: string[]; usedBy: string[] }>;
      toneProfile: {
        base: string;
        examples: string;
        codeSnippets?: string;
        avoidPatterns: string[];
      };
      audienceCalibration: {
        assumedKnowledge: string[];
        painPoints: string[];
        desiredOutcome: string;
      };
    }

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

    /**
     * 构建深度规划提示词消息
     */
    function buildDeepPlanMessages(
      title: string,
      description: string,
      bookScale: string,
      userFeedback?: string,
    ): ChatMessage[] {
      const config = getScaleConfig(bookScale);
      const chapters = config?.chapters || 17;

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

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

    请对本书做全面的策划分析。`;

      if (userFeedback) {
        userPrompt += `\n\n## 用户反馈（请据此调整规划）\n${userFeedback}`;
      }

      return [
        { role: 'system', content: DEEP_PLAN_SYSTEM_PROMPT },
        { role: 'user', content: userPrompt },
      ];
    }

    /**
     * 解析 AI 返回的深度规划 JSON
     */
    function parseDeepPlanResponse(response: string): DeepBookPlan | 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('[DeepPlanNode] 未找到 JSON');
          return null;
        }

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

        // 验证必要字段
        if (typeof data.genLevel !== 'number' || ![1, 2, 3].includes(data.genLevel)) {
          console.warn('[DeepPlanNode] genLevel 无效，使用默认值 2');
          data.genLevel = 2;
        }

        return {
          genLevel: data.genLevel || 2,
          bookType: data.bookType || '',
          bookTypeAnalysis: data.bookTypeAnalysis || data.reasoning || '',
          writingStyle: data.writingStyle || '',
          structureLogic: data.structureLogic || '',
          contentDepth: data.contentDepth || '',
          targetAudienceAnalysis: data.targetAudienceAnalysis || '',
          reasoning: data.reasoning || '',
          goldenThread: data.goldenThread || '',
          narrativeArc: data.narrativeArc || {},
          crossReferences: data.crossReferences || {},
          toneProfile: {
            base: data.toneProfile?.base || '',
            examples: data.toneProfile?.examples || '',
            codeSnippets: data.toneProfile?.codeSnippets,
            avoidPatterns: data.toneProfile?.avoidPatterns || [],
          },
          audienceCalibration: {
            assumedKnowledge: data.audienceCalibration?.assumedKnowledge || [],
            painPoints: data.audienceCalibration?.painPoints || [],
            desiredOutcome: data.audienceCalibration?.desiredOutcome || '',
          },
        };
      } catch (err) {
        console.error('[DeepPlanNode] 解析失败:', err);
        return null;
      }
    }

    /**
     * 深度规划节点
     * 作为新策略的第一个节点运行
     */
    export async function deepPlanBookNode(state: typeof GraphState.State): Promise<Partial<typeof GraphState.State>> {
      console.log('[DeepPlanNode] 开始深度规划, bookId:', state.bookId);

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

        const messages = buildDeepPlanMessages(title, description, state.bookScale, (state as any).userFeedback || '');
        const response = await callLLMWithMessages(messages);
        const plan = parseDeepPlanResponse(response);

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

        // 用户明确选了层级 → 保留用户选择，AI 不能覆盖
        const userDefinedLevel = state.userSpecifiedGenLevel !== undefined;
        const finalGenLevel = userDefinedLevel
          ? (state.userSpecifiedGenLevel ?? state.genLevel)
          : plan.genLevel;

        if (userDefinedLevel && finalGenLevel !== plan.genLevel) {
          console.log(`[DeepPlanNode] 用户选定 genLevel=${finalGenLevel}，AI 建议=${plan.genLevel}，保留用户选择`);
        }

        console.log(`[DeepPlanNode] 规划完成: genLevel=${finalGenLevel}, 风格=${plan.writingStyle}, 主线=${plan.goldenThread}`);

        // 持久化到数据库
        await bookStore.update(state.bookId, {
          bookAnalysis: JSON.stringify(plan),
          progress: PROGRESS.OUTLINE_DONE,
        });

        return {
          progress: PROGRESS.OUTLINE_DONE,
          genLevel: finalGenLevel,
          bookPlan: JSON.stringify(plan),
        };
      } catch (error) {
        console.error('[DeepPlanNode] 规划失败:', error);
        await bookStore.update(state.bookId, {
          progress: PROGRESS.OUTLINE_DONE,
        });
        return { progress: PROGRESS.OUTLINE_DONE };
      }
    }