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