Просмотр исходного кода

refactor: 拆分工具函数和状态定义 - 提取utils.ts和graph.ts

MyFramework User 4 месяцев назад
Родитель
Сommit
e060578966

+ 68 - 0
server/src/modules/book-generator/graph.ts

@@ -0,0 +1,68 @@
+/**
+ * LangGraph 状态定义和工作流
+ */
+
+import { Annotation, StateGraph, END } from '@langchain/langgraph';
+
+// ============ 状态定义(借鉴 OpenMAIC Annotation 模式)============
+
+/**
+ * 进度 reducer:只增不减,防止中间步骤回退导致进度丢失
+ */
+const maxReducer = (prev: number, update: number) => Math.max(prev, update);
+
+/**
+ * 章节完成数 reducer:累加而非覆盖
+ */
+const appendReducer = <T>(prev: T[], update: T | T[] | undefined) => {
+  if (!update) return prev;
+  const items = Array.isArray(update) ? update : [update];
+  return [...prev, ...items];
+};
+
+export const GraphState = Annotation.Root({
+  bookId: Annotation<string>({
+    reducer: (_prev, update) => update ?? _prev,
+    default: () => '' as string,
+  }),
+  topic: Annotation<string>({
+    reducer: (_prev, update) => update ?? _prev,
+    default: () => '' as string,
+  }),
+  bookScale: Annotation<string>({
+    reducer: (_prev, update) => update ?? _prev,
+    default: () => '标准教程' as string,
+  }),
+  description: Annotation<string>({
+    reducer: (_prev, update) => update ?? _prev,
+    default: () => '' as string,
+  }),
+  /** 当前正在处理的章节号 */
+  currentChapter: Annotation<number>({
+    reducer: maxReducer,
+    default: () => 0,
+  }),
+  /** 已成功完成的章节数(只增不减) */
+  completedChapters: Annotation<number[]>({
+    reducer: appendReducer,
+    default: () => [] as number[],
+  }),
+  finished: Annotation<boolean>({
+    reducer: (_prev, update) => update ?? _prev,
+    default: () => false,
+  }),
+  error: Annotation<string | undefined>({
+    reducer: (_prev, update) => update ?? _prev,
+    default: () => undefined,
+  }),
+  /** 生成进度 0-100,只增不减 */
+  progress: Annotation<number>({
+    reducer: maxReducer,
+    default: () => 0,
+  }),
+  /** 失败章节列表,通过 reducer 合并而非覆盖 */
+  failedChapters: Annotation<number[]>({
+    reducer: appendReducer,
+    default: () => [] as number[],
+  }),
+});

+ 46 - 0
server/src/modules/book-generator/nodes/outline.node.ts

@@ -0,0 +1,46 @@
+/**
+ * 大纲生成节点
+ */
+
+import { GraphState } from '../graph';
+import { bookStore } from '../book-generator.store';
+import { callLLMWithMessages } from '../../../services/llm';
+import { parseOutline } from '../parsers/outline.parser';
+import { buildOutlineMessages } from '../prompts/builder';
+import { PROGRESS } from '../utils';
+
+export async function generateOutlineNode(state: typeof GraphState.State): Promise<Partial<typeof GraphState.State>> {
+  console.log('[LangGraph] 生成大纲, bookId:', state.bookId, 'scale:', state.bookScale);
+
+  const messages = buildOutlineMessages(state.topic, state.bookScale, state.description);
+
+  try {
+    const response = await callLLMWithMessages(messages);
+    const outline = parseOutline(response);
+    if (!outline) throw new Error('大纲解析失败');
+
+    const totalChapters = outline.chapters.length;
+    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 => ({
+      number: c.number, 
+      title: c.title, 
+      summary: c.summary, 
+      keyPoints: c.keyPoints, 
+      estimatedWords: c.estimatedWords,
+    })));
+
+    console.log('[LangGraph] 大纲生成完成,章节数:', outline.chapters.length);
+    return { progress: PROGRESS.OUTLINE_DONE };
+  } catch (error) {
+    console.error('[LangGraph] 大纲生成失败:', error);
+    return { 
+      error: error instanceof Error ? error.message : '失败', 
+      finished: true 
+    };
+  }
+}

+ 23 - 0
server/src/modules/book-generator/utils.ts

@@ -0,0 +1,23 @@
+/**
+ * 工具函数
+ */
+
+/**
+ * 计算中文字数
+ */
+export function countWords(text: string): number {
+  return (text.match(/[\u4e00-\u9fa5]/g) || []).length;
+}
+
+/**
+ * 进度常量
+ */
+export const PROGRESS = {
+  OUTLINE_DONE: 5,
+  SECTIONS_DONE: 10,
+  SUBSECTIONS_DONE: 15,
+  CONTENT_START: 15,
+  CONTENT_END: 95,
+  FOREWORD_DONE: 95,
+  AFTERWORD_DONE: 100,
+};