Преглед изворни кода

refactor: 删除旧的langgraph-generator.ts - 已完成拆分

MyFramework User пре 4 месеци
родитељ
комит
dc77b754a5
1 измењених фајлова са 0 додато и 1408 уклоњено
  1. 0 1408
      server/src/modules/book-generator/langgraph-generator.ts

+ 0 - 1408
server/src/modules/book-generator/langgraph-generator.ts

@@ -1,1408 +0,0 @@
-/**
- * LangGraph 书籍生成器
- * 使用 @langchain/langgraph v1.2.8 API
- * 状态通过数据库传递,LangGraph 只负责流程控制
- * 支持生成过程中额度监控和中断保存
- */
-
-import { BookGenerationState, ChapterResult } from './langgraph-types';
-import { OutlineChapter } from './book-generator.types';
-import { bookStore } from './book-generator.store';
-import { prisma } from '../../models';
-import { Annotation, StateGraph, END } from '@langchain/langgraph';
-import { callLLM, callLLMWithMessages, callLLMWithTools, ChatMessage } from '../../services/llm';
-import { createBookTools } from '../../services/llm/book-tools';
-import { checkQuotaForWords, markGenerationInterrupted, getGeneratedWordCount } from '../subscription/subscription.service';
-
-// ============ 状态定义(借鉴 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];
-};
-
-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[],
-  }),
-});
-
-// ============ 进度常量 ============
-
-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 = {
-  '800': { min: 1, max: 1 }, '2000': { min: 1, max: 1 }, '5000': { min: 1, max: 1 },
-  小册子: { min: 3, max: 8 }, 标准教程: { min: 5, max: 15 }, 系统教材: { min: 10, max: 20 },
-};
-
-const SCALE_DESC: Record<string, string> = {
-  '800': '短文,约800字', '2000': '短文,约2000字', '5000': '短文,约5000字',
-  小册子: '小册子,1-5万字,3-8章', 标准教程: '标准教程,5-15万字,5-15章', 系统教材: '系统教材,15-30万字,10-20章',
-};
-
-// ============ 提示词模板(借鉴 OpenMAIC:System 详细定义角色,User 传递参数)============
-
-/**
- * 大纲生成系统提示词
- * OpenMAIC 模式:系统提示词详细,用户提示词简洁
- */
-const OUTLINE_SYSTEM_PROMPT = `你是一位专业的图书策划编辑,擅长为各类主题设计清晰、严谨、有逻辑递进的书籍大纲。
-
-## 你的职责
-根据用户提供的书名、规模和主题,设计一份完整的书籍大纲。
-
-## 工作要求
-1. 分析主题复杂度,在允许的章节范围内决定最合适的章节数
-2. 章节之间必须有清晰的逻辑递进关系(由浅入深、由理论到实践等)
-3. 每章的知识点要具体,不能泛泛而谈
-4. 预估字数要符合章节内容量
-5. 核心知识点应独立成章,不要过度合并
-
-## ⚠️ 输出格式要求(非常重要)
-1. 必须且只能返回纯 JSON,不要任何解释、思考过程或其他文字
-2. 不要使用 markdown 代码块标记(\`\`\`json)
-3. 不要包含任何标签(如<details>、<think> 等)
-4. 直接从 { 开始,到 } 结束
-5. JSON 必须合法,可以被 JSON.parse 解析
-
-## JSON 格式
-{
-  "mainTheme": "主题一句话描述",
-  "structureLogic": "章节组织逻辑说明",
-  "chapters": [
-    {
-      "number": 1,
-      "title": "章节标题",
-      "summary": "章节摘要(2-3句话)",
-      "keyPoints": ["知识点1", "知识点2"],
-      "estimatedWords": 2000
-    }
-  ]
-}`;
-
-/**
- * 章节写作系统提示词(支持工具调用)
- */
-const CHAPTER_SYSTEM_PROMPT = `你是一位专业的书籍作者,擅长撰写结构严谨、内容丰富、通俗易懂的章节内容。
-
-## 你的职责
-撰写指定章节的正文内容。
-
-## 工具说明
-你可以调用以下工具来提升写作质量:
-- get_existing_chapters:查看已写好的章节摘要,避免内容重复,保持风格一致
-- get_book_outline:查看全书大纲,了解当前章节在全书中的位置
-- report_chapter_issue:发现内容问题时报告,然后重新生成改进版本
-
-## 写作要求
-1. 先调用 get_book_outline 了解整体结构
-2. 如果不是第一章,调用 get_existing_chapters 查看前几章内容,避免重复
-3. 正文要有深度,不能只是列举要点,要有解释、案例、分析
-4. 字数尽量达到预估字数要求
-5. 直接输出正文内容,不要输出任何 JSON 或 markdown 格式说明`;
-
-// ============ 节/小节生成提示词============
-
-/**
- * 节大纲生成系统提示词
- * 根据章的大纲,生成该章下各节的详细大纲
- */
-const SECTION_SYSTEM_PROMPT = `你是一位专业的图书策划编辑,擅长为书籍章节设计清晰的内部结构。
-
-## 你的职责
-根据给定的章主题和概述,设计该章下各节的详细大纲。
-
-## 输出格式
-必须返回 JSON,不要包含任何 markdown 代码块标记:
-{
-  "sections": [
-    {
-      "number": 1,
-      "title": "节标题",
-      "summary": "本节概述(1-2句话)",
-      "keyPoints": ["要点1", "要点2"],
-      "estimatedWords": 1500
-    }
-  ]
-}`;
-
-/**
- * 小节大纲生成系统提示词
- * 根据节的大纲,生成该节下各小节的详细大纲
- */
-const SUBSECTION_SYSTEM_PROMPT = `你是一位专业的图书策划编辑,擅长为书籍小节设计具体的知识点结构。
-
-## 你的职责
-根据给定的节主题和概述,设计该节下各小节的详细大纲。
-
-## 输出格式
-必须返回 JSON,不要包含任何 markdown 代码块标记:
-{
-  "subsections": [
-    {
-      "number": 1,
-      "title": "小节标题",
-      "summary": "本小节概述(1-2句话)",
-      "keyPoints": ["要点1", "要点2"],
-      "estimatedWords": 800
-    }
-  ]
-}`;
-
-// ============ 前言/后记提示词(OpenMAIC 模式:System 详细定义,User 传参)============
-
-/**
- * 前言系统提示词
- * 借鉴 OpenMAIC:详细定义角色 + 格式约束 + 质量要求
- */
-const FOREWORD_SYSTEM_PROMPT = `你是一位资深作家,擅长撰写引人入胜的书籍前言。
-
-## 你的职责
-为书籍撰写一篇精彩的前言,吸引读者继续阅读。
-
-## 质量要求
-1. 篇幅 300-500 字,语言流畅有感染力
-2. 开篇要有亮点,能抓住读者注意力(可用故事、名言、问题等切入)
-3. 简要介绍本书的主题、价值和特色,但不剧透核心内容
-4. 语气真诚、有热情,让读者感受到作者对主题的热爱
-5. 可以分享写作缘由或目标读者定位
-
-## 格式要求
-- 直接输出正文,不要加"前言"标题
-- 不要使用 markdown 格式标记(不加 #、**、- 等)
-- 不要在结尾写"希望读者..."之类的客套话
-- 直接开始叙述,第一句就要有吸引力`;
-
-/**
- * 后记系统提示词
- */
-const AFTERWORD_SYSTEM_PROMPT = `你是一位资深作家,擅长撰写令人回味的书籍后记。
-
-## 你的职责
-为书籍撰写一篇有余韵的后记,让读者有所收获和思考。
-
-## 质量要求
-1. 篇幅 300-500 字,收尾有力
-2. 可以总结全书核心观点,但要用自己的话提炼而非重复
-3. 分享写作过程中的感悟、挑战或有趣发现
-4. 给读者留下思考空间或行动指引
-5. 语气真诚、谦逊,有深度但不说教
-
-## 格式要求
-- 直接输出正文,不要加"后记"标题
-- 不要使用 markdown 格式标记
-- 不要写"感谢读者"之类的套话
-- 结尾要有力量感,可以是金句、问题或开放性思考`;
-
-/**
- * 小节内容写作提示词
- */
-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 格式标记`;
-
-// ============ 动态提示词配置表 ============
-
-/**
- * 书籍类型提示词配置
- */
-const BOOK_TYPE_PROMPTS: Record<string, string> = {
-  '教材': `
-## 教材编写要求
-1. 章节结构: 基础篇→核心篇→进阶篇→实践篇
-2. 每章应包含: 学习目标、正文内容、案例分析、本章小结、习题
-3. 知识点要系统全面,由浅入深,循序渐进
-4. 理论与实践相结合,注重可操作性
-5. 适合课堂教学使用,有明确的教学目标
-`,
-  '技术教程': `
-## 技术教程要求
-1. 章节结构: 环境搭建→基础知识→核心功能→进阶应用→实战项目
-2. 每章应包含: 知识点讲解、代码示例、实践练习、常见问题
-3. 步骤要详细清晰,读者可以直接跟着操作
-4. 代码示例要完整,有注释说明
-5. 注重实战,有完整的项目案例
-`,
-  '小说': `
-## 小说创作要求
-1. 章节结构: 开端→发展→高潮→结局(可根据类型调整)
-2. 每章应包含: 情节推进、人物塑造、冲突设置、悬念铺垫
-3. 情节要紧凑有张力,人物形象鲜明
-4. 注重故事节奏,有起承转合
-5. 结尾要有余韵,给读者留下想象空间
-`,
-  '商业': `
-## 商业书籍要求
-1. 章节结构: 提出问题→分析原因→解决方案→案例验证→行动指南
-2. 每章应包含: 核心观点、理论分析、实际案例、操作方法
-3. 观点要鲜明有说服力,有数据或案例支撑
-4. 注重实用性,读者可以直接应用
-5. 语言要通俗易懂,避免过于学术化
-`,
-  '科普': `
-## 科普读物要求
-1. 章节结构: 现象引入→原理揭秘→实际应用→未来展望
-2. 每章应包含: 趣味故事、科学原理、生活应用、知识拓展
-3. 语言要生动有趣,用比喻解释复杂概念
-4. 注重知识性和趣味性的平衡
-5. 激发读者的好奇心和探索欲
-`,
-};
-
-/**
- * 目标读者提示词配置
- */
-const AUDIENCE_PROMPTS: Record<string, string> = {
-  '儿童': `
-## 读者定位: 儿童(6-12岁)
-1. 语言要生动形象,充满趣味性
-2. 多用比喻、故事和生活中的例子
-3. 避免复杂的专业术语
-4. 句子要短小简单,易于理解
-5. 可以加入互动元素,激发好奇心
-`,
-  '青少年': `
-## 读者定位: 青少年(13-18岁)
-1. 语言要通俗易懂,但不失准确性
-2. 可以引入基本概念,但要解释清楚
-3. 多用年轻人感兴趣的话题和例子
-4. 注重启发思考,培养兴趣
-5. 难度适中,循序渐进
-`,
-  '初学者': `
-## 读者定位: 初学者
-1. 从最基础的概念开始讲解
-2. 避免使用过多专业术语,必要时解释术语
-3. 多用比喻和生活中的例子
-4. 每章知识点不要过多,循序渐进
-`,
-  '大学生': `
-## 读者定位: 大学生
-1. 可以系统性地讲解理论知识
-2. 适当使用专业术语,但首次出现时要解释
-3. 注重理论与实践相结合
-4. 有明确的章节学习目标和总结
-5. 可以加入思考题和练习题
-`,
-  '研究生': `
-## 读者定位: 研究生
-1. 可以直接使用专业术语,不需要过多解释
-2. 注重深度和广度,涵盖前沿技术/理论
-3. 可以有较多的高级内容和最佳实践
-4. 注重系统性和完整性
-5. 包含研究方法和学术讨论
-`,
-  '专业人士': `
-## 读者定位: 专业人士
-1. 可以直接使用专业术语,不需要过多解释
-2. 注重深度和广度,涵盖前沿技术/理论
-3. 可以有较多的高级内容和最佳实践
-4. 注重系统性和完整性
-`,
-  '大众读者': `
-## 读者定位: 大众读者
-1. 语言要通俗易懂,避免过于专业
-2. 多讲故事和案例,少讲理论
-3. 注重趣味性和可读性
-4. 每章内容要贴近生活,有实用性
-`,
-};
-
-/**
- * 内容深度提示词配置
- */
-const DEPTH_PROMPTS: Record<string, string> = {
-  '入门': `
-## 内容深度: 入门级
-1. 覆盖面广但深度适中
-2. 重点在于建立知识框架
-3. 不涉及过于复杂的细节
-4. 适合快速了解全貌
-5. 多用比喻和简单例子
-`,
-  '基础': `
-## 内容深度: 基础级
-1. 讲解基本概念和原理
-2. 循序渐进,由浅入深
-3. 每个概念都要解释清楚
-4. 有基础的实例说明
-5. 适合有一定了解的读者
-`,
-  '进阶': `
-## 内容深度: 进阶级
-1. 在基础之上深入讲解
-2. 包含高级技巧和最佳实践
-3. 有实际案例分析
-4. 适合有一定基础的读者
-`,
-  '高级': `
-## 内容深度: 高级级
-1. 深入探讨专业领域
-2. 包含前沿技术和高级应用
-3. 有大量技术细节和优化技巧
-4. 适合专业人士深入学习
-`,
-  '专家': `
-## 内容深度: 专家级
-1. 深入探讨底层原理
-2. 包含前沿研究和高级应用
-3. 有大量技术细节和优化技巧
-4. 适合专业人士和研究人员
-`,
-};
-
-/**
- * 行业领域提示词配置
- */
-const INDUSTRY_PROMPTS: Record<string, string> = {
-  'IT/计算机': `
-## 行业领域: IT/计算机
-1. 可以包含代码示例和技术架构图
-2. 注重技术的实用性和可操作性
-3. 关注最新技术趋势和最佳实践
-4. 有完整的开发环境搭建指导
-`,
-  '金融': `
-## 行业领域: 金融
-1. 注重数据分析和案例研究
-2. 包含实际的市场案例
-3. 关注风险管理和合规要求
-4. 有明确的投资策略或方法论
-`,
-  '医学': `
-## 行业领域: 医学
-1. 注重科学性和严谨性
-2. 基于循证医学
-3. 包含临床案例
-4. 关注最新医学进展
-`,
-  '教育': `
-## 行业领域: 教育
-1. 注重教学方法和学习效果
-2. 包含教学设计案例
-3. 关注教育心理学应用
-4. 有可操作的教学方案
-`,
-};
-
-// ============ 工具函数 ============
-
-function countWords(text: string): number {
-  return (text.match(/[\u4e00-\u9fa5]/g) || []).length;
-}
-
-/**
- * 需求分析: 从用户描述中提取关键信息
- */
-interface BookRequirements {
-  bookType?: string;      // 教材/技术教程/小说/商业/科普
-  audience?: string;      // 初学者/专业人士/大众读者
-  depth?: string;         // 入门/进阶/专家
-  industry?: string;      // IT/金融/医学/教育
-  specialNeeds?: string[]; // 特殊需求: 有案例/有代码/有习题等
-}
-
-function analyzeRequirements(topic: string, description?: string): BookRequirements {
-  const text = `${topic} ${description || ''}`.toLowerCase();
-  const req: BookRequirements = {
-    specialNeeds: [],
-  };
-
-  // 分析书籍类型
-  if (/教材|课程|教学|大学|学校/i.test(text)) {
-    req.bookType = '教材';
-  } else if (/教程|实战|开发|编程|代码/i.test(text)) {
-    req.bookType = '技术教程';
-  } else if (/小说|故事|文学|传记|散文/i.test(text)) {
-    req.bookType = '小说';
-  } else if (/商业|管理|营销|创业|投资|经济/i.test(text)) {
-    req.bookType = '商业';
-  } else if (/科普|百科|科学|探索|揭秘/i.test(text)) {
-    req.bookType = '科普';
-  }
-
-  // 分析目标读者
-  if (/儿童|小学生/i.test(text)) {
-    req.audience = '儿童';
-  } else if (/青少年|中学生|高中生/i.test(text)) {
-    req.audience = '青少年';
-  } else if (/初学者|入门|新手|零基础|小白/i.test(text)) {
-    req.audience = '初学者';
-  } else if (/大学生|本科生/i.test(text)) {
-    req.audience = '大学生';
-  } else if (/研究生|硕士|博士/i.test(text)) {
-    req.audience = '研究生';
-  } else if (/专业|高级|深入|专家|研究/i.test(text)) {
-    req.audience = '专业人士';
-  } else if (/大众|通俗|普及|广泛/i.test(text)) {
-    req.audience = '大众读者';
-  }
-  
-  // 分析知识难度
-  if (/入门|零基础|简单|易懂/i.test(text)) {
-    req.depth = '入门';
-  } else if (/基础|基本概念|循序渐进/i.test(text)) {
-    req.depth = '基础';
-  } else if (/进阶|深入|系统全面/i.test(text)) {
-    req.depth = '进阶';
-  } else if (/高级|专业深入|前沿/i.test(text)) {
-    req.depth = '高级';
-  } else if (/专家|研究|底层原理/i.test(text)) {
-    req.depth = '专家';
-  }
-
-  // 分析行业领域
-  if (/计算机|IT|编程|软件|互联网|AI|人工智能/i.test(text)) {
-    req.industry = 'IT/计算机';
-  } else if (/金融|股票|投资|银行|证券/i.test(text)) {
-    req.industry = '金融';
-  } else if (/医学|医疗|健康|临床|疾病/i.test(text)) {
-    req.industry = '医学';
-  } else if (/教育|教学|学习|学校|课程/i.test(text)) {
-    req.industry = '教育';
-  }
-
-  // 分析特殊需求
-  if (/案例|实例/i.test(text)) req.specialNeeds.push('有案例');
-  if (/代码|示例/i.test(text)) req.specialNeeds.push('有代码');
-  if (/习题|练习|作业/i.test(text)) req.specialNeeds.push('有习题');
-  if (/图表|插图|配图/i.test(text)) req.specialNeeds.push('有图表');
-  if (/项目|实战|实践/i.test(text)) req.specialNeeds.push('有项目实战');
-
-  return req;
-}
-
-/**
- * 动态组装提示词
- */
-function buildDynamicPrompt(topic: string, bookScale: string, description?: string): string {
-  const req = analyzeRequirements(topic, description);
-  
-  // 基础提示词
-  let prompt = OUTLINE_SYSTEM_PROMPT;
-
-  // 根据书籍类型添加
-  if (req.bookType && BOOK_TYPE_PROMPTS[req.bookType]) {
-    prompt += BOOK_TYPE_PROMPTS[req.bookType];
-  }
-
-  // 根据目标读者添加
-  if (req.audience && AUDIENCE_PROMPTS[req.audience]) {
-    prompt += AUDIENCE_PROMPTS[req.audience];
-  }
-
-  // 根据内容深度添加
-  if (req.depth && DEPTH_PROMPTS[req.depth]) {
-    prompt += DEPTH_PROMPTS[req.depth];
-  }
-
-  // 根据行业领域添加
-  if (req.industry && INDUSTRY_PROMPTS[req.industry]) {
-    prompt += INDUSTRY_PROMPTS[req.industry];
-  }
-
-  // 特殊需求
-  if (req.specialNeeds.length > 0) {
-    prompt += `\n## 特殊要求\n`;
-    prompt += `根据用户需求,本书应包含: ${req.specialNeeds.join('、')}\n`;
-  }
-
-  console.log(`[DynamicPrompt] 检测到需求:`, req);
-  
-  return prompt;
-}
-
-/**
- * 构建大纲生成消息(动态提示词)
- */
-function buildOutlineMessages(topic: string, bookScale: string, description?: string): ChatMessage[] {
-  const range = SCALE_CHAPTER_RANGE[bookScale as keyof typeof SCALE_CHAPTER_RANGE] || { min: 5, max: 10 };
-  
-  // 使用动态提示词
-  const systemPrompt = buildDynamicPrompt(topic, bookScale, description);
-  
-  return [
-    { role: 'system', content: systemPrompt },
-    {
-      role: 'user',
-      content: `书名:《${topic}》\n规模:${SCALE_DESC[bookScale] || '中篇'}\n章节范围:${range.min}-${range.max}章\n${description ? `需求描述:${description}\n` : ''}\n请生成书籍大纲。`,
-    },
-  ];
-}
-
-/**
- * 构建节大纲生成消息
- */
-function buildSectionMessages(chapterTitle: string, chapterSummary: string, keyPoints: string[]): ChatMessage[] {
-  return [
-    { role: 'system', content: SECTION_SYSTEM_PROMPT },
-    {
-      role: 'user',
-      content: `章标题:${chapterTitle}\n章概述:${chapterSummary}\n核心知识点:${keyPoints.join('、')}\n\n请为该章设计节的大纲(通常每章 2-4 节)。`,
-    },
-  ];
-}
-
-/**
- * 构建小节大纲生成消息
- */
-function buildSubsectionMessages(sectionTitle: string, sectionSummary: string, keyPoints: string[]): ChatMessage[] {
-  return [
-    { role: 'system', content: SUBSECTION_SYSTEM_PROMPT },
-    {
-      role: 'user',
-      content: `节标题:${sectionTitle}\n节概述:${sectionSummary}\n核心知识点:${keyPoints.join('、')}\n\n请为该节设计小节的大纲(通常每节 2-4 小节)。`,
-    },
-  ];
-}
-
-/**
- * 构建章节写作消息(配合工具调用)
- */
-function buildChapterMessages(topic: string, chapter: OutlineChapter): ChatMessage[] {
-  return [
-    { role: 'system', content: CHAPTER_SYSTEM_PROMPT },
-    {
-      role: 'user',
-      content: `请撰写《${topic}》第${chapter.number}章。\n章节标题:${chapter.title}\n章节概述:${chapter.summary}\n核心知识点:${chapter.keyPoints.join('、')}\n预估字数:${chapter.estimatedWords}字`,
-    },
-  ];
-}
-
-function buildForewordMessages(topic: string): ChatMessage[] {
-  return [
-    { role: 'system', content: FOREWORD_SYSTEM_PROMPT },
-    { role: 'user', content: `请为《${topic}》撰写前言。` },
-  ];
-}
-
-function buildAfterwordMessages(topic: string): ChatMessage[] {
-  return [
-    { role: 'system', content: AFTERWORD_SYSTEM_PROMPT },
-    { role: 'user', content: `请为《${topic}》撰写后记。` },
-  ];
-}
-
-function parseOutline(jsonStr: string): any {
-  if (!jsonStr || typeof jsonStr !== 'string') {
-    console.error('[OutlineParser] 输入为空或非字符串');
-    return null;
-  }
-
-  try {
-    // 尝试多种 JSON 提取策略(借鉴 OpenMAIC partial-json 思路)
-    let data: any;
-
-    // 策略1:直接解析(最理想情况,LLM 直接输出纯 JSON)
-    try {
-      data = JSON.parse(jsonStr.trim());
-    } catch {
-      // 策略2:提取 JSON 对象(处理 LLM 加了 markdown 标记或多余文字)
-      const match = jsonStr.match(/\{[\s\S]*\}/);
-      if (!match) {
-        console.error('[OutlineParser] 未找到 JSON 对象');
-        return null;
-      }
-      try {
-        data = JSON.parse(match[0]);
-      } catch (parseErr) {
-        console.error('[OutlineParser] JSON 解析失败:', parseErr);
-        return null;
-      }
-    }
-
-    // 验证必要字段
-    if (!data || typeof data !== 'object') {
-      console.error('[OutlineParser] 解析结果非对象');
-      return null;
-    }
-    if (!Array.isArray(data.chapters)) {
-      console.error('[OutlineParser] chapters 字段缺失或非数组');
-      // 尝试兼容:若顶层就是章节数组
-      if (Array.isArray(data)) {
-        data = { chapters: data };
-      } else {
-        return null;
-      }
-    }
-
-    return {
-      mainTheme: data.mainTheme || '主题待定',
-      structureLogic: data.structureLogic || '由浅入深',
-      chapters: data.chapters.map((c: any, i: number) => ({
-        number: c.number || i + 1,
-        title: c.title || `第${i + 1}章`,
-        summary: typeof c.summary === 'string' ? c.summary : '',
-        keyPoints: Array.isArray(c.keyPoints) ? c.keyPoints : [],
-        estimatedWords: typeof c.estimatedWords === 'number' ? c.estimatedWords : 1000,
-      })),
-    };
-  } catch (err) {
-    console.error('[OutlineParser] 未知错误:', err);
-    return null;
-  }
-}
-
-/**
- * 解析节大纲
- */
-function parseSections(jsonStr: string): { sections: any[] } | null {
-  if (!jsonStr || typeof jsonStr !== 'string') {
-    console.error('[SectionParser] 输入为空或非字符串');
-    return null;
-  }
-
-  try {
-    let data: any;
-    let cleanedStr = jsonStr.trim();
-
-    // 调试:打印前50个字符的编码
-    const debugStr = cleanedStr.substring(0, Math.min(50, cleanedStr.length));
-    console.log('[SectionParser] 原始前50字符:', JSON.stringify(debugStr));
-
-    // 清理 JSON 字符串中的非法控制字符和多余空白
-    cleanedStr = cleanedStr.replace(/[\x00-\x1F\x7F]/g, (char) => {
-      if (char === '\n') return ' ';
-      if (char === '\r') return ' ';
-      if (char === '\t') return ' ';
-      return ' ';
-    });
-
-    // 移除 markdown 代码块标记
-    cleanedStr = cleanedStr.replace(/```json\s*/gi, '').replace(/```\s*/g, '').trim();
-
-    // 移除思考标签
-    cleanedStr = cleanedStr.replace(/^[\s\S]*?<blockquote>\s*[\s\S]*?<\/blockquote>\s*/, '');
-    const secThinkIdx = cleanedStr.indexOf('</think>');
-    if (secThinkIdx === 0) cleanedStr = cleanedStr.substring(secThinkIdx + 8).trim();
-
-    console.log('[SectionParser] 清理后:', cleanedStr.substring(0, Math.min(100, cleanedStr.length)));
-
-    try {
-      data = JSON.parse(cleanedStr);
-    } catch (firstErr) {
-      console.log('[SectionParser] 首次解析失败,尝试正则提取');
-      // 尝试找到 JSON 对象
-      const match = cleanedStr.match(/\{[\s\S]*\}/);
-      if (!match) {
-        console.error('[SectionParser] 未找到 JSON 对象');
-        return null;
-      }
-      try {
-        const jsonCandidate = match[0];
-        console.log('[SectionParser] 正则提取:', jsonCandidate.substring(0, Math.min(100, jsonCandidate.length)));
-        data = JSON.parse(jsonCandidate);
-      } catch (parseErr) {
-        console.error('[SectionParser] JSON 解析失败:', parseErr, '原始:', cleanedStr.substring(0, 200));
-        return null;
-      }
-    }
-
-    if (!data || typeof data !== 'object') {
-      console.error('[SectionParser] 解析结果非对象');
-      return null;
-    }
-
-    // 兼容:如果顶层就是数组
-    if (Array.isArray(data)) {
-      data = { sections: data };
-    }
-
-    // 兼容多种字段名:sections, Sections, section_list 等
-    let sectionsArray = data.sections || data.Sections || data.section_list || data.chapter_sections;
-    
-    if (!Array.isArray(sectionsArray)) {
-      console.error('[SectionParser] sections 字段缺失或非数组,实际keys:', Object.keys(data));
-      return null;
-    }
-
-    return {
-      sections: sectionsArray.map((s: any, i: number) => ({
-        number: s.number || i + 1,
-        title: s.title || `第${i + 1}节`,
-        summary: typeof s.summary === 'string' ? s.summary : '',
-        keyPoints: Array.isArray(s.keyPoints) ? s.keyPoints : [],
-        estimatedWords: typeof s.estimatedWords === 'number' ? s.estimatedWords : 1000,
-      })),
-    };
-  } catch (err) {
-    console.error('[SectionParser] 未知错误:', err);
-    return null;
-  }
-}
-
-/**
- * 解析小节大纲
- */
-function parseSubsections(jsonStr: string): { subsections: any[] } | null {
-  if (!jsonStr || typeof jsonStr !== 'string') {
-    console.error('[SubsectionParser] 输入为空或非字符串');
-    return null;
-  }
-
-  try {
-    let data: any;
-    let cleanedStr = jsonStr.trim();
-
-    // 调试:打印前80个字符
-    console.log('[SubsectionParser] 原始前80字符:', JSON.stringify(cleanedStr.substring(0, 80)));
-
-    // 清理 JSON 字符串中的非法控制字符
-    cleanedStr = cleanedStr.replace(/[\x00-\x1F\x7F]/g, (char) => {
-      // 将控制字符替换为转义的换行符或空格
-      if (char === '\n') return '\\n';
-      if (char === '\r') return '\\r';
-      if (char === '\t') return '\\t';
-      return ' ';
-    });
-
-    // 移除 markdown 代码块标记
-    cleanedStr = cleanedStr.replace(/```json\s*/gi, '').replace(/```\s*/g, '').trim();
-
-    // 移除思考标签
-    cleanedStr = cleanedStr.replace(/^[\s\S]*?<blockquote>\s*[\s\S]*?<\/blockquote>\s*/, '');
-    // 如果以 开头,说明思考标签在JSON之前,需要移除
-    // 修复:</think>是结束标签,</think>是开始标签
-    const thinkEndIdx = cleanedStr.indexOf('</think>');
-    if (thinkEndIdx !== -1) {
-      cleanedStr = cleanedStr.substring(thinkEndIdx + 8).trim();
-    }
-
-    console.log('[SubsectionParser] 处理后前100字符:', JSON.stringify(cleanedStr.substring(0, 100)));
-
-    try {
-      data = JSON.parse(cleanedStr);
-    } catch {
-      // 使用括号计数法提取JSON对象
-      const firstBrace = cleanedStr.indexOf('{');
-      if (firstBrace === -1) {
-        console.error('[SubsectionParser] 未找到 JSON 对象');
-        return null;
-      }
-      let jsonCandidate = '';
-      let depth = 0;
-      let started = false;
-      for (let i = firstBrace; i < cleanedStr.length; i++) {
-        const ch = cleanedStr[i];
-        if (ch === '{') {
-          depth++;
-          started = true;
-        } else if (ch === '}') {
-          depth--;
-        }
-        if (started) jsonCandidate += ch;
-        if (started && depth === 0) break;
-      }
-      if (!jsonCandidate) {
-        console.error('[SubsectionParser] JSON 对象提取失败');
-        return null;
-      }
-      try {
-        data = JSON.parse(jsonCandidate);
-      } catch (parseErr) {
-        console.error('[SubsectionParser] JSON 解析失败:', parseErr);
-        return null;
-      }
-    }
-
-    if (!data || typeof data !== 'object') {
-      console.error('[SubsectionParser] 解析结果非对象');
-      return null;
-    }
-
-    // 兼容:如果顶层就是数组
-    if (Array.isArray(data)) {
-      data = { subsections: data };
-    }
-
-    // 兼容多种字段名:subsections, Subsections, subsection_list 等
-    let subsectionsArray = data.subsections || data.Subsections || data.subsection_list || data.section_subsections;
-    
-    if (!Array.isArray(subsectionsArray)) {
-      console.error('[SubsectionParser] subsections 字段缺失或非数组');
-      return null;
-    }
-
-    return {
-      subsections: subsectionsArray.map((s: any, i: number) => ({
-        number: s.number || i + 1,
-        title: s.title || `第${i + 1}小节`,
-        summary: typeof s.summary === 'string' ? s.summary : '',
-        keyPoints: Array.isArray(s.keyPoints) ? s.keyPoints : [],
-        estimatedWords: typeof s.estimatedWords === 'number' ? s.estimatedWords : 500,
-      })),
-    };
-  } catch (err) {
-    console.error('[SubsectionParser] 未知错误:', err);
-    return null;
-  }
-}
-
-// ============ LangGraph 节点 ============
-
-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 };
-  }
-}
-
-/**
- * 生成节大纲节点(二级大纲)
- * 为每章生成节的大纲
- */
-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 || []);
-
-      // 添加重试机制,最多重试2次
-      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) {
-          // 为每节创建数据库记录(level=2)
-          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}章的数据库记录`);
-          }
-
-          // 更新大纲 JSON
-          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 };
-  }
-}
-
-/**
- * 生成小节大纲节点(三级大纲)
- * 为每节生成小节的大纲
- */
-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 || []);
-
-          // 添加重试机制,最多重试2次
-          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) {
-              // 为每小节创建数据库记录(level=3)
-              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 };
-  }
-}
-
-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);
-
-  // 筛选出未完成内容的叶节点(支持断点续传)
-  // 叶节点判断:contentStatus !== 'completed' 表示内容未生成
-  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 chapters: ChapterResult[] = [];
-  const failedChapters: number[] = [];
-
-  // 创建书籍工具(让 LLM 可查询上下文、避免重复)
-  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);
-    }
-  }
-
-  // 更新所有父节和章的状态为 completed
-  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
-  };
-}
-
-/**
- * 构建小节内容生成消息
- */
-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}字
-
-请撰写该小节的正文内容。`,
-    },
-  ];
-}
-
-// ============ LangGraph 节点 ============
-
-async function writeForewordNode(state: typeof GraphState.State): Promise<Partial<typeof GraphState.State>> {
-  console.log('[LangGraph] 生成前言, bookId:', state.bookId);
-  try {
-    const messages = buildForewordMessages(state.topic);
-    const foreword = await callLLMWithMessages(messages);
-    await bookStore.update(state.bookId, { foreword, progress: PROGRESS.FOREWORD_DONE });
-    return { progress: PROGRESS.FOREWORD_DONE };
-  } catch (error) {
-    console.error('[LangGraph] 前言生成失败:', error);
-    return { progress: PROGRESS.FOREWORD_DONE };
-  }
-}
-
-async function writeAfterwordNode(state: typeof GraphState.State): Promise<Partial<typeof GraphState.State>> {
-  console.log('[LangGraph] 生成后记, bookId:', state.bookId);
-  try {
-    const messages = buildAfterwordMessages(state.topic);
-    const afterword = await callLLMWithMessages(messages);
-    await bookStore.update(state.bookId, { afterword });
-    return { finished: true, progress: PROGRESS.AFTERWORD_DONE };
-  } catch (error) {
-    console.error('[LangGraph] 后记生成失败:', error);
-    return { finished: true, progress: PROGRESS.AFTERWORD_DONE };
-  }
-}
-
-// ============ 创建工作流 ============
-
-function createGraph() {
-  const workflow = new StateGraph(GraphState)
-    .addNode('generate_outline', generateOutlineNode)
-    .addNode('generate_sections', generateSectionsNode)
-    .addNode('generate_subsections', generateSubsectionsNode)
-    .addNode('write_chapters', writeChaptersNode)
-    .addNode('write_foreword', writeForewordNode)
-    .addNode('write_afterword', writeAfterwordNode)
-    .setEntryPoint('generate_outline')
-    // 流程:章大纲 → 节大纲 → 小节大纲 → 内容生成 → 前言 → 后记
-    .addEdge('generate_outline', 'generate_sections')
-    .addEdge('generate_sections', 'generate_subsections')
-    .addEdge('generate_subsections', 'write_chapters')
-    .addEdge('write_chapters', 'write_foreword')
-    .addEdge('write_foreword', 'write_afterword')
-    .addEdge('write_afterword', END);
-
-  return workflow.compile();
-}
-
-// ============ 主类 ============
-
-export class LangGraphBookGenerator {
-  private graph = createGraph();
-
-  async generate(bookId: string, topic: string, bookScale: string = 'medium'): Promise<void> {
-    const initialState = { bookId, topic, bookScale, currentChapter: 0, finished: false, error: undefined, progress: 0 };
-    await bookStore.update(bookId, { status: 'generating', progress: 0 });
-
-    try {
-      const stream = await this.graph.stream(initialState);
-      for await (const step of stream) {
-        console.log('[LangGraph] Step:', Object.keys(step));
-      }
-      await bookStore.update(bookId, { status: 'completed', progress: 100 });
-      await bookStore.publishAlbum(bookId);
-      console.log('[LangGraph] 书籍生成完成');
-    } catch (error) {
-      console.error('[LangGraph] 生成失败:', error);
-      await bookStore.update(bookId, { status: 'failed', errorMsg: error instanceof Error ? error.message : '生成失败' });
-    }
-  }
-}
-
-export const langGraphGenerator = new LangGraphBookGenerator();
-
-// 重新导出 book-type-config 中的函数供外部使用
-export { getScaleConfig } from './book-type-config';