Jelajahi Sumber

refactor: 拆分内容生成节点 - 提取nodes/content.node.ts

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

+ 191 - 0
server/src/modules/book-generator/nodes/content.node.ts

@@ -0,0 +1,191 @@
+/**
+ * 内容生成节点
+ * 生成叶节点(小节)的实际内容
+ */
+
+import { GraphState } from '../graph';
+import { bookStore } from '../book-generator.store';
+import { prisma } from '../../../models';
+import { callLLMWithMessages, callLLMWithTools, ChatMessage } from '../../../services/llm';
+import { createBookTools } from '../../../services/llm/book-tools';
+import { checkQuotaForWords, markGenerationInterrupted, getGeneratedWordCount } from '../../subscription/subscription.service';
+import { SUBSECTION_CONTENT_SYSTEM_PROMPT } from '../prompts/templates';
+import { PROGRESS, countWords } from '../utils';
+
+/**
+ * 构建小节内容生成消息
+ */
+function buildSubsectionContentMessages(
+  topic: string, 
+  chapterTitle: string, 
+  chapterSummary: string, 
+  sectionTitle: string, 
+  sectionSummary: string, 
+  subsection: any
+): ChatMessage[] {
+  return [
+    { role: 'system', content: SUBSECTION_CONTENT_SYSTEM_PROMPT },
+    {
+      role: 'user',
+      content: `书名:《${topic}》
+章标题:${chapterTitle}
+章概述:${chapterSummary || ''}
+节标题:${sectionTitle}
+节概述:${sectionSummary}
+小节标题:${subsection.title}
+小节概述:${subsection.summary || ''}
+核心知识点:${(subsection.keyPoints || []).join('、')}
+预估字数:${subsection.estimatedWords || 500}字
+
+请撰写该小节的正文内容。`,
+    },
+  ];
+}
+
+/**
+ * 内容生成节点
+ * 为所有叶节点(level=3小节)生成实际内容
+ */
+export async function writeChaptersNode(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.CONTENT_END };
+  }
+
+  const bookIdNum = parseInt(state.bookId);
+
+  // 筛选出未完成内容的叶节点(支持断点续传)
+  const allSubsections = await prisma.bookChapter.findMany({
+    where: { bookId: bookIdNum, level: 3 },
+    orderBy: [
+      { parentId: 'asc' },
+      { number: 'asc' }
+    ],
+  });
+
+  const subsections = allSubsections.filter((s: any) => s.contentStatus !== 'completed');
+  const completedCount = allSubsections.length - subsections.length;
+
+  if (completedCount > 0) {
+    console.log(`[LangGraph] ✅ 跳过 ${completedCount} 个已生成内容的小节,待生成 ${subsections.length} 个`);
+  }
+
+  if (subsections.length === 0) {
+    console.log('[LangGraph] ✅ 所有小节内容已完成,跳过内容生成');
+    return { finished: true, progress: PROGRESS.CONTENT_END };
+  }
+
+  // 构建父节点映射
+  const chapterMap = new Map<number, any>();
+  const sectionMap = new Map<number, any>();
+  
+  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 === 2) sectionMap.set(c.id, c);
+  });
+
+  const failedChapters: number[] = [];
+  const bookTools = createBookTools(state.bookId, bookStore);
+
+  let currentWordCount = await getGeneratedWordCount(state.bookId);
+  console.log(`[LangGraph] 当前已生成字数: ${currentWordCount}`);
+  console.log(`[LangGraph] 待生成小节数: ${subsections.length}`);
+
+  const totalSubsections = allSubsections.length;
+  let completedSubsections = completedCount;
+
+  for (const subsection of subsections) {
+    const parentSection = sectionMap.get(subsection.parentId || 0);
+    const parentChapter = parentSection ? chapterMap.get(parentSection.parentId || 0) : null;
+
+    // 额度监控
+    try {
+      const quotaCheck = await checkQuotaForWords(book.userId || 1, currentWordCount);
+      if (!quotaCheck.sufficient) {
+        console.warn(`[LangGraph] ⚠️ 额度不足,中断生成: ${quotaCheck.reason}`);
+        await markGenerationInterrupted(state.bookId, subsection.number - 1, currentWordCount);
+        return {
+          currentChapter: subsection.number,
+          progress: Math.round((completedSubsections / totalSubsections) * 80) + 15,
+          error: `额度不足中断:${quotaCheck.reason},已保存进度`
+        };
+      }
+    } catch (quotaErr) {
+      console.warn(`[LangGraph] 额度检查失败,继续生成`);
+    }
+
+    const chapterTitle = parentChapter?.title || '未知章';
+    const sectionTitle = parentSection?.title || '未知节';
+    const chapterSummary = parentChapter?.summary || '';
+    const sectionSummary = parentSection?.summary || '';
+
+    console.log(`[LangGraph] 生成${chapterTitle} - ${sectionTitle} - ${subsection.title}`);
+    const messages = buildSubsectionContentMessages(
+      state.topic, 
+      chapterTitle, 
+      chapterSummary, 
+      sectionTitle, 
+      sectionSummary, 
+      subsection
+    );
+
+    try {
+      let content: string;
+      try {
+        const result = await callLLMWithTools(messages, bookTools);
+        content = result.text;
+        if (result.toolCalls.length > 0) {
+          console.log(`[LangGraph] 小节「${subsection.title}」使用了 ${result.toolCalls.length} 次工具调用`);
+        }
+      } catch (toolErr: any) {
+        console.log(`[LangGraph] 工具调用不可用,降级为普通调用`);
+        content = await callLLMWithMessages(messages);
+      }
+
+      const wordCount = countWords(content);
+      currentWordCount += wordCount;
+      completedSubsections++;
+
+      await bookStore.updateChapterById(subsection.id, { 
+        content, 
+        wordCount, 
+        contentStatus: 'completed'
+      });
+
+      const progress = Math.round((completedSubsections / totalSubsections) * (PROGRESS.CONTENT_END - PROGRESS.CONTENT_START)) + PROGRESS.CONTENT_START;
+      await bookStore.update(state.bookId, { progress, status: 'generating' });
+      console.log(`[LangGraph] 小节「${subsection.title}」内容完成 (${completedSubsections}/${totalSubsections}),累计${currentWordCount}字,进度${progress}%`);
+
+    } catch (error) {
+      const errorMsg = error instanceof Error ? error.message : '失败';
+      await bookStore.updateChapterById(subsection.id, { 
+        contentStatus: 'failed', 
+        contentError: errorMsg 
+      });
+      failedChapters.push(subsection.number);
+      console.error(`[LangGraph] 小节「${subsection.title}」内容失败:`, errorMsg);
+    }
+  }
+
+  // 更新所有父节和章的状态
+  await prisma.bookChapter.updateMany({
+    where: { bookId: bookIdNum, level: { in: [1, 2] } },
+    data: { status: 'completed' }
+  });
+
+  return {
+    currentChapter: chaptersAndSections.filter(c => c.level === 1).length,
+    progress: PROGRESS.CONTENT_END,
+    error: failedChapters.length > 0 ? `小节${failedChapters.join(',')}失败` : undefined
+  };
+}