Jelajahi Sumber

refactor: 拆分节和小节大纲节点 - 提取nodes/sections.node.ts

MyFramework User 4 bulan lalu
induk
melakukan
e4083ec995
1 mengubah file dengan 220 tambahan dan 0 penghapusan
  1. 220 0
      server/src/modules/book-generator/nodes/sections.node.ts

+ 220 - 0
server/src/modules/book-generator/nodes/sections.node.ts

@@ -0,0 +1,220 @@
+/**
+ * 节和小节大纲生成节点
+ */
+
+import { GraphState } from '../graph';
+import { bookStore } from '../book-generator.store';
+import { prisma } from '../../../models';
+import { callLLMWithMessages } from '../../../services/llm';
+import { parseSections } from '../parsers/section.parser';
+import { parseSubsections } from '../parsers/subsection.parser';
+import { buildSectionMessages, buildSubsectionMessages } from '../prompts/builder';
+import { PROGRESS } from '../utils';
+
+/**
+ * 生成节大纲节点(二级大纲)
+ * 为每章生成节的大纲
+ */
+export async function generateSectionsNode(state: typeof GraphState.State): Promise<Partial<typeof GraphState.State>> {
+  console.log('[LangGraph] 生成节大纲, bookId:', state.bookId);
+
+  const book = await bookStore.getById(state.bookId);
+  if (!book || !book.outline) {
+    console.log('[LangGraph] 无大纲,跳过节生成');
+    return { finished: true, progress: PROGRESS.SECTIONS_DONE };
+  }
+
+  try {
+    const updatedChapters = [];
+
+    for (const chapter of book.outline.chapters) {
+      console.log(`[LangGraph] 为第${chapter.number}章「${chapter.title}」生成节大纲`);
+      const messages = buildSectionMessages(chapter.title, chapter.summary, chapter.keyPoints || []);
+
+      let parsed = null;
+      let lastError = null;
+      for (let retry = 0; retry < 3; retry++) {
+        try {
+          const response = await callLLMWithMessages(messages);
+          parsed = parseSections(response);
+
+          if (parsed && parsed.sections.length > 0) {
+            console.log(`[LangGraph] 第${chapter.number}章节大纲解析成功,共${parsed.sections.length}节`);
+            break;
+          } else {
+            console.warn(`[LangGraph] 第${chapter.number}章节大纲解析失败(第${retry + 1}次),重试中...`);
+            lastError = new Error('解析结果为空');
+          }
+        } catch (sectionErr) {
+          console.error(`[LangGraph] 第${chapter.number}章节大纲生成失败(第${retry + 1}次):`, sectionErr);
+          lastError = sectionErr;
+        }
+      }
+
+      if (parsed && parsed.sections.length > 0) {
+        const bookIdNum = parseInt(state.bookId);
+        const chapterRecord = await prisma.bookChapter.findFirst({
+          where: { 
+            bookId: bookIdNum,
+            parentId: null,
+            level: 1, 
+            number: chapter.number 
+          }
+        });
+
+        if (chapterRecord) {
+          for (const section of parsed.sections) {
+            await bookStore.createChapterItem(bookIdNum, {
+              number: section.number,
+              title: section.title,
+              summary: section.summary,
+              keyPoints: section.keyPoints,
+              estimatedWords: section.estimatedWords,
+            }, chapterRecord.id, 2);
+
+            console.log(`[LangGraph]   - 第${chapter.number}章第${section.number}节「${section.title}」`);
+          }
+        } else {
+          console.error(`[LangGraph] 未找到第${chapter.number}章的数据库记录`);
+        }
+
+        updatedChapters.push({
+          ...chapter,
+          sections: parsed.sections,
+        });
+      } else {
+        console.warn(`[LangGraph] 第${chapter.number}章节大纲解析失败,跳过。错误:`, lastError);
+        updatedChapters.push(chapter);
+      }
+    }
+
+    const updatedOutline = { ...book.outline, chapters: updatedChapters };
+    await bookStore.update(state.bookId, {
+      outlineJson: JSON.stringify(updatedOutline),
+      progress: PROGRESS.SECTIONS_DONE,
+    });
+
+    console.log('[LangGraph] 节大纲生成完成');
+    return { progress: PROGRESS.SECTIONS_DONE };
+  } catch (error) {
+    console.error('[LangGraph] 节大纲生成失败:', error);
+    return { error: error instanceof Error ? error.message : '失败', finished: true };
+  }
+}
+
+/**
+ * 生成小节大纲节点(三级大纲)
+ * 为每节生成小节的大纲
+ */
+export async function generateSubsectionsNode(state: typeof GraphState.State): Promise<Partial<typeof GraphState.State>> {
+  console.log('[LangGraph] 生成小节大纲, bookId:', state.bookId);
+
+  const book = await bookStore.getById(state.bookId);
+  if (!book || !book.outline) {
+    console.log('[LangGraph] 无大纲,跳过小节生成');
+    return { finished: true, progress: PROGRESS.SUBSECTIONS_DONE };
+  }
+
+  try {
+    const updatedChapters = [];
+
+    for (const chapter of book.outline.chapters) {
+      const updatedSections = [];
+
+      if (chapter.sections && chapter.sections.length > 0) {
+        for (const section of chapter.sections) {
+          console.log(`[LangGraph] 为第${chapter.number}章第${section.number}节「${section.title}」生成小节大纲`);
+          const messages = buildSubsectionMessages(section.title, section.summary || '', section.keyPoints || []);
+
+          let parsed = null;
+          let lastError = null;
+          for (let retry = 0; retry < 3; retry++) {
+            try {
+              const response = await callLLMWithMessages(messages);
+              parsed = parseSubsections(response);
+
+              if (parsed && parsed.subsections.length > 0) {
+                console.log(`[LangGraph] 第${chapter.number}章第${section.number}节小节大纲解析成功,共${parsed.subsections.length}小节`);
+                break;
+              } else {
+                console.warn(`[LangGraph] 第${chapter.number}章第${section.number}节小节大纲解析失败(第${retry + 1}次),重试中...`);
+                lastError = new Error('解析结果为空');
+              }
+            } catch (subErr) {
+              console.error(`[LangGraph] 第${chapter.number}章第${section.number}节小节大纲生成失败(第${retry + 1}次):`, subErr);
+              lastError = subErr;
+            }
+          }
+
+          if (parsed && parsed.subsections.length > 0) {
+            const bookIdNum = parseInt(state.bookId);
+            const chapterRecord = await prisma.bookChapter.findFirst({
+              where: { 
+                bookId: bookIdNum,
+                parentId: null,
+                level: 1, 
+                number: chapter.number 
+              }
+            });
+            
+            if (!chapterRecord) {
+              console.error(`[LangGraph] 未找到第${chapter.number}章的数据库记录`);
+              updatedSections.push(section);
+              continue;
+            }
+            
+            const sectionRecord = await prisma.bookChapter.findFirst({
+              where: { 
+                bookId: bookIdNum,
+                parentId: chapterRecord.id, 
+                level: 2, 
+                number: section.number 
+              }
+            });
+
+            if (sectionRecord) {
+              for (const subsection of parsed.subsections) {
+                await bookStore.createChapterItem(bookIdNum, {
+                  number: subsection.number,
+                  title: subsection.title,
+                  summary: subsection.summary,
+                  keyPoints: subsection.keyPoints,
+                  estimatedWords: subsection.estimatedWords,
+                }, sectionRecord.id, 3);
+
+                console.log(`[LangGraph]     - 第${chapter.number}章第${section.number}节第${subsection.number}小节「${subsection.title}」`);
+              }
+            } else {
+              console.error(`[LangGraph] 未找到第${chapter.number}章第${section.number}节的数据库记录`);
+            }
+
+            updatedSections.push({
+              ...section,
+              subsections: parsed.subsections,
+            });
+          } else {
+            console.warn(`[LangGraph] 第${chapter.number}章第${section.number}节小节大纲解析失败,跳过。错误:`, lastError);
+            updatedSections.push(section);
+          }
+        }
+      }
+
+      updatedChapters.push({
+        ...chapter,
+        sections: updatedSections.length > 0 ? updatedSections : chapter.sections,
+      });
+    }
+
+    const updatedOutline = { ...book.outline, chapters: updatedChapters };
+    await bookStore.update(state.bookId, {
+      outlineJson: JSON.stringify(updatedOutline),
+      progress: PROGRESS.SUBSECTIONS_DONE,
+    });
+
+    console.log('[LangGraph] 小节大纲生成完成');
+    return { progress: PROGRESS.SUBSECTIONS_DONE };
+  } catch (error) {
+    console.error('[LangGraph] 小节大纲生成失败:', error);
+    return { error: error instanceof Error ? error.message : '失败', finished: true };
+  }
+}