Parcourir la source

refactor: 优化书籍生成器 - 添加断点续传+进度常量+代码整理

- 修复断点续传:过滤已完成小节,支持中断后继续
- 修复进度计算:从已完成数量开始,避免进度回退
- 添加PROGRESS常量:替换所有魔法数字
- 移动SUBSECTION_CONTENT_SYSTEM_PROMPT到顶部提示词区域
- 优化父节点映射:简化查询逻辑
- 改进日志输出:显示跳过的小节数量
MyFramework User il y a 4 mois
Parent
commit
154069da1e
1 fichiers modifiés avec 73 ajouts et 45 suppressions
  1. 73 45
      server/src/modules/book-generator/langgraph-generator.ts

+ 73 - 45
server/src/modules/book-generator/langgraph-generator.ts

@@ -77,6 +77,18 @@ const GraphState = Annotation.Root({
   }),
   }),
 });
 });
 
 
+// ============ 进度常量 ============
+
+const PROGRESS = {
+  OUTLINE_DONE: 5,
+  SECTIONS_DONE: 10,
+  SUBSECTIONS_DONE: 15,
+  CONTENT_START: 15,
+  CONTENT_END: 95,
+  FOREWORD_DONE: 95,
+  AFTERWORD_DONE: 100,
+};
+
 // ============ 规模配置 ============
 // ============ 规模配置 ============
 
 
 const SCALE_CHAPTER_RANGE = {
 const SCALE_CHAPTER_RANGE = {
@@ -243,6 +255,27 @@ const AFTERWORD_SYSTEM_PROMPT = `你是一位资深作家,擅长撰写令人
 - 不要写"感谢读者"之类的套话
 - 不要写"感谢读者"之类的套话
 - 结尾要有力量感,可以是金句、问题或开放性思考`;
 - 结尾要有力量感,可以是金句、问题或开放性思考`;
 
 
+/**
+ * 小节内容写作提示词
+ */
+const SUBSECTION_CONTENT_SYSTEM_PROMPT = `你是一位专业的书籍作者,擅长撰写结构严谨、内容丰富、通俗易懂的小节内容。
+
+## 你的职责
+撰写指定小节的正文内容。
+
+## 工具说明
+你可以调用以下工具来提升写作质量:
+- get_existing_chapters:查看已写好的章节摘要,避免内容重复,保持风格一致
+- get_book_outline:查看全书大纲,了解当前章节在全书中的位置
+- report_chapter_issue:发现内容问题时报告,然后重新生成改进版本
+
+## 写作要求
+1. 先调用 get_book_outline 了解整体结构
+2. 调用 get_existing_chapters 查看前几章内容,避免重复
+3. 正文要有深度,不能只是列举要点,要有解释、案例、分析
+4. 字数尽量达到预估字数要求
+5. 直接输出正文内容,不要输出任何 JSON 或 markdown 格式标记`;
+
 // ============ 动态提示词配置表 ============
 // ============ 动态提示词配置表 ============
 
 
 /**
 /**
@@ -893,13 +926,13 @@ async function generateOutlineNode(state: typeof GraphState.State): Promise<Part
     if (!outline) throw new Error('大纲解析失败');
     if (!outline) throw new Error('大纲解析失败');
 
 
     const totalChapters = outline.chapters.length;
     const totalChapters = outline.chapters.length;
-    await bookStore.update(state.bookId, { totalChapters, outlineJson: JSON.stringify(outline), status: 'planning', progress: 5 });
+    await bookStore.update(state.bookId, { totalChapters, outlineJson: JSON.stringify(outline), status: 'planning', progress: PROGRESS.OUTLINE_DONE });
     await bookStore.createChapters(state.bookId, outline.chapters.map(c => ({
     await bookStore.createChapters(state.bookId, outline.chapters.map(c => ({
       number: c.number, title: c.title, summary: c.summary, keyPoints: c.keyPoints, estimatedWords: c.estimatedWords,
       number: c.number, title: c.title, summary: c.summary, keyPoints: c.keyPoints, estimatedWords: c.estimatedWords,
     })));
     })));
 
 
     console.log('[LangGraph] 大纲生成完成,章节数:', outline.chapters.length);
     console.log('[LangGraph] 大纲生成完成,章节数:', outline.chapters.length);
-    return { progress: 5 };
+    return { progress: PROGRESS.OUTLINE_DONE };
   } catch (error) {
   } catch (error) {
     console.error('[LangGraph] 大纲生成失败:', error);
     console.error('[LangGraph] 大纲生成失败:', error);
     return { error: error instanceof Error ? error.message : '失败', finished: true };
     return { error: error instanceof Error ? error.message : '失败', finished: true };
@@ -916,7 +949,7 @@ async function generateSectionsNode(state: typeof GraphState.State): Promise<Par
   const book = await bookStore.getById(state.bookId);
   const book = await bookStore.getById(state.bookId);
   if (!book || !book.outline) {
   if (!book || !book.outline) {
     console.log('[LangGraph] 无大纲,跳过节生成');
     console.log('[LangGraph] 无大纲,跳过节生成');
-    return { finished: true, progress: 10 };
+    return { finished: true, progress: PROGRESS.SECTIONS_DONE };
   }
   }
 
 
   try {
   try {
@@ -992,11 +1025,11 @@ async function generateSectionsNode(state: typeof GraphState.State): Promise<Par
     const updatedOutline = { ...book.outline, chapters: updatedChapters };
     const updatedOutline = { ...book.outline, chapters: updatedChapters };
     await bookStore.update(state.bookId, {
     await bookStore.update(state.bookId, {
       outlineJson: JSON.stringify(updatedOutline),
       outlineJson: JSON.stringify(updatedOutline),
-      progress: 10,
+      progress: PROGRESS.SECTIONS_DONE,
     });
     });
 
 
     console.log('[LangGraph] 节大纲生成完成');
     console.log('[LangGraph] 节大纲生成完成');
-    return { progress: 10 };
+    return { progress: PROGRESS.SECTIONS_DONE };
   } catch (error) {
   } catch (error) {
     console.error('[LangGraph] 节大纲生成失败:', error);
     console.error('[LangGraph] 节大纲生成失败:', error);
     return { error: error instanceof Error ? error.message : '失败', finished: true };
     return { error: error instanceof Error ? error.message : '失败', finished: true };
@@ -1013,7 +1046,7 @@ async function generateSubsectionsNode(state: typeof GraphState.State): Promise<
   const book = await bookStore.getById(state.bookId);
   const book = await bookStore.getById(state.bookId);
   if (!book || !book.outline) {
   if (!book || !book.outline) {
     console.log('[LangGraph] 无大纲,跳过小节生成');
     console.log('[LangGraph] 无大纲,跳过小节生成');
-    return { finished: true, progress: 15 };
+    return { finished: true, progress: PROGRESS.SUBSECTIONS_DONE };
   }
   }
 
 
   try {
   try {
@@ -1114,11 +1147,11 @@ async function generateSubsectionsNode(state: typeof GraphState.State): Promise<
     const updatedOutline = { ...book.outline, chapters: updatedChapters };
     const updatedOutline = { ...book.outline, chapters: updatedChapters };
     await bookStore.update(state.bookId, {
     await bookStore.update(state.bookId, {
       outlineJson: JSON.stringify(updatedOutline),
       outlineJson: JSON.stringify(updatedOutline),
-      progress: 15,
+      progress: PROGRESS.SUBSECTIONS_DONE,
     });
     });
 
 
     console.log('[LangGraph] 小节大纲生成完成');
     console.log('[LangGraph] 小节大纲生成完成');
-    return { progress: 15 };
+    return { progress: PROGRESS.SUBSECTIONS_DONE };
   } catch (error) {
   } catch (error) {
     console.error('[LangGraph] 小节大纲生成失败:', error);
     console.error('[LangGraph] 小节大纲生成失败:', error);
     return { error: error instanceof Error ? error.message : '失败', finished: true };
     return { error: error instanceof Error ? error.message : '失败', finished: true };
@@ -1131,13 +1164,13 @@ async function writeChaptersNode(state: typeof GraphState.State): Promise<Partia
   const book = await bookStore.getById(state.bookId);
   const book = await bookStore.getById(state.bookId);
   if (!book || !book.outline) {
   if (!book || !book.outline) {
     console.log('[LangGraph] 无大纲或书籍,跳过章节生成');
     console.log('[LangGraph] 无大纲或书籍,跳过章节生成');
-    return { finished: true, progress: 90 };
+    return { finished: true, progress: PROGRESS.CONTENT_END };
   }
   }
 
 
   const bookIdNum = parseInt(state.bookId);
   const bookIdNum = parseInt(state.bookId);
 
 
   // 从数据库获取所有小节(level=3)
   // 从数据库获取所有小节(level=3)
-  const subsections = await prisma.bookChapter.findMany({
+  const allSubsections = await prisma.bookChapter.findMany({
     where: { bookId: bookIdNum, level: 3 },
     where: { bookId: bookIdNum, level: 3 },
     orderBy: [
     orderBy: [
       { parentId: 'asc' },  // 先按父节点排序
       { parentId: 'asc' },  // 先按父节点排序
@@ -1154,20 +1187,32 @@ async function writeChaptersNode(state: typeof GraphState.State): Promise<Partia
     }
     }
   });
   });
 
 
+  // 筛选出未完成的小节(pending 或 failed),支持断点续传
+  const subsections = allSubsections.filter(s => s.status !== 'completed');
+  const completedCount = allSubsections.length - subsections.length;
+
+  if (completedCount > 0) {
+    console.log(`[LangGraph] ✅ 跳过 ${completedCount} 个已完成的小节,待生成 ${subsections.length} 个`);
+  }
+
   if (subsections.length === 0) {
   if (subsections.length === 0) {
-    console.log('[LangGraph] 没有小节,跳过内容生成');
-    return { finished: true, progress: 90 };
+    console.log('[LangGraph] ✅ 所有小节已完成,跳过内容生成');
+    return { finished: true, progress: 95 };
   }
   }
 
 
   // 构建父节点映射,便于获取上下文
   // 构建父节点映射,便于获取上下文
-  const allChapters = await prisma.bookChapter.findMany({
-    where: { bookId: bookIdNum },
-    orderBy: { number: 'asc' }
-  });
-
   const chapterMap = new Map<number, any>();
   const chapterMap = new Map<number, any>();
   const sectionMap = new Map<number, any>();
   const sectionMap = new Map<number, any>();
-  allChapters.forEach(c => {
+  
+  // 重新查询章节和节来构建映射
+  const chaptersAndSections = await prisma.bookChapter.findMany({
+    where: { 
+      bookId: bookIdNum, 
+      level: { in: [1, 2] } 
+    }
+  });
+  
+  chaptersAndSections.forEach(c => {
     if (c.level === 1) chapterMap.set(c.id, c);
     if (c.level === 1) chapterMap.set(c.id, c);
     if (c.level === 2) sectionMap.set(c.id, c);
     if (c.level === 2) sectionMap.set(c.id, c);
   });
   });
@@ -1183,7 +1228,7 @@ async function writeChaptersNode(state: typeof GraphState.State): Promise<Partia
   console.log(`[LangGraph] 当前已生成字数: ${currentWordCount}`);
   console.log(`[LangGraph] 当前已生成字数: ${currentWordCount}`);
   console.log(`[LangGraph] 待生成小节数: ${subsections.length}`);
   console.log(`[LangGraph] 待生成小节数: ${subsections.length}`);
 
 
-  // 计算总小节数用于进度
+  // 计算总小节数用于进度(从已完成数量开始)
   const totalSubsections = allSubsections.length;
   const totalSubsections = allSubsections.length;
   let completedSubsections = completedCount; // 从已完成的数量开始
   let completedSubsections = completedCount; // 从已完成的数量开始
 
 
@@ -1237,7 +1282,7 @@ async function writeChaptersNode(state: typeof GraphState.State): Promise<Partia
       await bookStore.updateChapterById(subsection.id, { content, wordCount, status: 'completed' });
       await bookStore.updateChapterById(subsection.id, { content, wordCount, status: 'completed' });
 
 
       // 更新进度
       // 更新进度
-      const progress = Math.round((completedSubsections / totalSubsections) * 80) + 15;
+      const progress = Math.round((completedSubsections / totalSubsections) * (PROGRESS.CONTENT_END - PROGRESS.CONTENT_START)) + PROGRESS.CONTENT_START;
       await bookStore.update(state.bookId, { progress, status: 'generating' });
       await bookStore.update(state.bookId, { progress, status: 'generating' });
       console.log(`[LangGraph] 小节「${subsection.title}」完成 (${completedSubsections}/${totalSubsections}),累计${currentWordCount}字,进度${progress}%`);
       console.log(`[LangGraph] 小节「${subsection.title}」完成 (${completedSubsections}/${totalSubsections}),累计${currentWordCount}字,进度${progress}%`);
 
 
@@ -1256,8 +1301,8 @@ async function writeChaptersNode(state: typeof GraphState.State): Promise<Partia
   });
   });
 
 
   return {
   return {
-    currentChapter: allChapters.length,
-    progress: 90,
+    currentChapter: chaptersAndSections.filter(c => c.level === 1).length,
+    progress: PROGRESS.CONTENT_END,
     error: failedChapters.length > 0 ? `小节${failedChapters.join(',')}失败` : undefined
     error: failedChapters.length > 0 ? `小节${failedChapters.join(',')}失败` : undefined
   };
   };
 }
 }
@@ -1285,35 +1330,18 @@ function buildSubsectionContentMessages(topic: string, chapterTitle: string, cha
   ];
   ];
 }
 }
 
 
-// 小节内容写作提示词
-const SUBSECTION_CONTENT_SYSTEM_PROMPT = `你是一位专业的书籍作者,擅长撰写结构严谨、内容丰富、通俗易懂的小节内容。
-
-## 你的职责
-撰写指定小节的正文内容。
-
-## 工具说明
-你可以调用以下工具来提升写作质量:
-- get_existing_chapters:查看已写好的章节摘要,避免内容重复,保持风格一致
-- get_book_outline:查看全书大纲,了解当前章节在全书中的位置
-- report_chapter_issue:发现内容问题时报告,然后重新生成改进版本
-
-## 写作要求
-1. 先调用 get_book_outline 了解整体结构
-2. 调用 get_existing_chapters 查看前几章内容,避免重复
-3. 正文要有深度,不能只是列举要点,要有解释、案例、分析
-4. 字数尽量达到预估字数要求
-5. 直接输出正文内容,不要输出任何 JSON 或 markdown 格式标记`;
+// ============ LangGraph 节点 ============
 
 
 async function writeForewordNode(state: typeof GraphState.State): Promise<Partial<typeof GraphState.State>> {
 async function writeForewordNode(state: typeof GraphState.State): Promise<Partial<typeof GraphState.State>> {
   console.log('[LangGraph] 生成前言, bookId:', state.bookId);
   console.log('[LangGraph] 生成前言, bookId:', state.bookId);
   try {
   try {
     const messages = buildForewordMessages(state.topic);
     const messages = buildForewordMessages(state.topic);
     const foreword = await callLLMWithMessages(messages);
     const foreword = await callLLMWithMessages(messages);
-    await bookStore.update(state.bookId, { foreword, progress: 95 });
-    return { progress: 95 };
+    await bookStore.update(state.bookId, { foreword, progress: PROGRESS.FOREWORD_DONE });
+    return { progress: PROGRESS.FOREWORD_DONE };
   } catch (error) {
   } catch (error) {
     console.error('[LangGraph] 前言生成失败:', error);
     console.error('[LangGraph] 前言生成失败:', error);
-    return { progress: 95 };
+    return { progress: PROGRESS.FOREWORD_DONE };
   }
   }
 }
 }
 
 
@@ -1323,10 +1351,10 @@ async function writeAfterwordNode(state: typeof GraphState.State): Promise<Parti
     const messages = buildAfterwordMessages(state.topic);
     const messages = buildAfterwordMessages(state.topic);
     const afterword = await callLLMWithMessages(messages);
     const afterword = await callLLMWithMessages(messages);
     await bookStore.update(state.bookId, { afterword });
     await bookStore.update(state.bookId, { afterword });
-    return { finished: true, progress: 100 };
+    return { finished: true, progress: PROGRESS.AFTERWORD_DONE };
   } catch (error) {
   } catch (error) {
     console.error('[LangGraph] 后记生成失败:', error);
     console.error('[LangGraph] 后记生成失败:', error);
-    return { finished: true, progress: 100 };
+    return { finished: true, progress: PROGRESS.AFTERWORD_DONE };
   }
   }
 }
 }