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