Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 | /** * 逐章处理节点(perChapterNode) * 每个章节作为一个独立单元:结构生成 + 内容生成一次完成 * 章节之间可并行执行 * * 适用于策略3(per-chapter) * * 工作方式: * 1. 先生成全书章大纲 * 2. 然后为每章(并行)生成该章内部结构(节/小节)+ 所有内容 */ import { GraphState } from '../graph'; import { bookStore } from '../book-generator.store'; import { callLLMWithMessages, ChatMessage } from '../../../services/llm'; import { callLLMWithRetry, executeNodeWithTimeout, FAULT_TOLERANCE_CONFIG } from '../fault-tolerance'; import { PROGRESS } from '../utils'; import { parseOutline } from '../parsers/outline.parser'; import { buildOutlineMessages } from '../prompts/builder'; import { prisma } from '../../../models'; import { withGlobalLLMConcurrency } from '../utils/llm-concurrency'; /** 最大并行数(可通过环境变量 LLM_BOOK_CONCURRENCY 配置) */ const MAX_CONCURRENCY = (() => { const env = process.env['LLM_BOOK_CONCURRENCY']; if (env) { const p = parseInt(env, 10); if (!isNaN(p) && p > 0) return p; } return 5; })(); /** * 滚动摘要窗口大小:保留最近 N 章的完整摘要,更早的章压缩为一行概要。 * 避免到第30章时提示词包含前29章全量标题,Token 爆炸。 */ const ROLLING_WINDOW_SIZE = 3; /** * 构建滚动摘要上下文 * * 最近 ROLLING_WINDOW_SIZE 章:完整列出标题 + 摘要 * 更早的章:压缩为一行概要(只列标题) * * 这样第30章时上下文只包含最近3章详情 + 前27章概览,大幅节省 Token。 */ function buildRollingContext( previousChapters: Array<string | { number: number; title: string; summary?: string }> ): string { if (!previousChapters || previousChapters.length === 0) return ''; // 规范化输入(兼容旧调用传 string[]) const normalized = previousChapters.map((ch, i) => { if (typeof ch === 'string') { return { number: i + 1, title: ch, summary: undefined as string | undefined }; } return ch; }); if (normalized.length <= ROLLING_WINDOW_SIZE) { // 还不够窗口大小,全部列出 const lines = normalized.map(c => `- 第${c.number}章: ${c.title}${c.summary ? `(${c.summary.substring(0, 80)})` : ''}` ); return `\n## 前面已完成的章节\n${lines.join('\n')}`; } // 拆分:远章(压缩)+ 近章(完整) const distant = normalized.slice(0, normalized.length - ROLLING_WINDOW_SIZE); const recent = normalized.slice(-ROLLING_WINDOW_SIZE); const parts: string[] = []; // 远章压缩为一行概览 const distantTitles = distant.map(c => `第${c.number}章「${c.title}」`).join(' → '); parts.push(`\n## 前面已完成的章节(第1-${distant[distant.length - 1].number}章概要)`); parts.push(`${distantTitles}`); // 近章完整列出 parts.push(`\n## 最近章节(保持连贯性)`); parts.push(recent.map(c => `- 第${c.number}章: ${c.title}${c.summary ? `\n 摘要:${c.summary}` : ''}` ).join('\n')); return parts.join('\n'); } /** * 构建单章生成提示词(包括节/小节结构 + 正文内容) * * @param previousChapters 前面章节列表,每条 { number, title, summary? } * 如果传入 string[](旧签名兼容),自动转为 {number, title} 格式 */ function buildChapterPrompt( chapterTitle: string, chapterSummary: string, bookTitle: string, bookDescription: string, genLevel: number, previousChapters: Array<string | { number: number; title: string; summary?: string }>, bookPlan?: any ): ChatMessage[] { const levelInstruction = genLevel >= 3 ? '请在正文中先列出节标题,每节下列出小节标题,然后为每小节写出完整正文。' : genLevel === 2 ? '请在正文中先列出节标题,然后为每节写出完整正文。' : '直接写出本章完整正文,无需分节。'; // 构建滚动摘要上下文 const prevContext = buildRollingContext(previousChapters); const planContext = bookPlan ? `\n## 全书规划\n写作风格:${bookPlan.writingStyle || '未指定'}\n结构逻辑:${bookPlan.structureLogic || '未指定'}\n内容深度:${bookPlan.contentDepth || '未指定'}` : ''; const systemPrompt = `你是一位专业的书籍作者。请为指定章节生成完整内容。 ## 全书信息 - 书名:${bookTitle} - 内容:${bookDescription} ## 当前章节 章标题:${chapterTitle} 章摘要:${chapterSummary} ## 写作要求 ${levelInstruction} ## 结构要求 - genLevel >= 2:正文需包含 2-4 个节,每个节 500-1000 字 - genLevel >= 3:每节下包含 2-3 个小节,每个小节 300-500 字 - genLevel = 1:直接写 1500-2500 字的完整正文,不分节 ## 格式要求 - 使用 Markdown 格式 - 节标题用 ###,小节标题用 #### - 正文要详细、有深度,不能只是要点列表 - 字数尽量达到预估要求${prevContext}${planContext} ## 输出格式 必须返回纯 JSON,不要任何额外文字: { "content": "本章完整正文(Markdown格式,包含标题层级)", "wordCount": 1500 }`; return [ { role: 'system', content: systemPrompt }, { role: 'user', content: `请为「${chapterTitle}」生成完整内容。` }, ]; } /** * 逐章处理节点 * 先获全书章大纲,然后为每章独立生成内部结构+内容 */ export async function perChapterNode(state: typeof GraphState.State): Promise<Partial<typeof GraphState.State>> { console.log('[PerChapter] 逐章处理, bookId:', state.bookId, 'genLevel:', state.genLevel); const bookIdNum = parseInt(state.bookId); try { const book = await bookStore.getById(state.bookId); if (!book || !book.outline || !book.outline.chapters || book.outline.chapters.length === 0) { console.error('[PerChapter] 无大纲'); return { finished: true, error: '无大纲' }; } const chapters = book.outline.chapters; const totalChapters = chapters.length; const completedChapters: number[] = []; const failedChapters: number[] = []; // 读取规划结果 let bookPlan: any = null; if (state.bookPlan) { try { bookPlan = JSON.parse(state.bookPlan); } catch { /* ok */ } } // 逐章串行处理(避免并行竞争导致的写入丢失) for (const chapter of chapters) { const chapterIdx = chapter.number - 1; // 收集前几章的标题+摘要用于滚动上下文(含摘要以减少 Token 浪费) const prevTitles = chapters .slice(0, chapterIdx) .filter(c => completedChapters.includes(c.number)) .map(c => ({ number: c.number, title: c.title, summary: c.summary })); try { console.log(`[PerChapter] 开始处理第${chapter.number}章「${chapter.title}」`); const messages = buildChapterPrompt( chapter.title, chapter.summary || '', book.title, book.description || '', state.genLevel, prevTitles, bookPlan ); const response = await withGlobalLLMConcurrency(() => callLLMWithRetry(messages, undefined, { bookId: state.bookId, nodeId: `chapter_${chapter.number}`, attempt: 0, maxAttempts: FAULT_TOLERANCE_CONFIG.aiRetry.maxRetries, }) ); console.log(`[PerChapter] 第${chapter.number}章 AI 响应长度: ${response.length}`); // 解析返回的 JSON 内容 const parsed = parseChapterContent(response); if (!parsed || !parsed.content) { throw new Error(`章节内容解析失败,response=${response.substring(0, 200)}`); } console.log(`[PerChapter] 第${chapter.number}章内容解析成功,字数: ${parsed.wordCount || parsed.content.length}`); // 如果 genLevel >= 2,需要先生成章节内部结构(level=2/3的节点) if (state.genLevel >= 2) { const chapterRecord = await prisma.bookChapter.findFirst({ where: { bookId: bookIdNum, number: chapter.number, level: 1, parentId: 0 }, }); if (chapterRecord) { console.log(`[PerChapter] 第${chapter.number}章找到记录 id=${chapterRecord.id},创建内部结构`); await createInternalStructure(bookIdNum, chapterRecord.id, parsed.content, state.genLevel, chapter.title); } else { console.warn(`[PerChapter] 第${chapter.number}章未找到数据库记录`); } } // 保存内容到章节(内容始终存到 level=1 的章记录) const targetChapter = await prisma.bookChapter.findFirst({ where: { bookId: bookIdNum, number: chapter.number, level: 1, parentId: 0 }, }); if (targetChapter) { const contentLen = parsed.content.length; await bookStore.updateChapterById(targetChapter.id, { content: parsed.content, wordCount: parsed.wordCount || contentLen, genStage: 'content_completed', }); // 验证内容是否真的写入了数据库 const verify = await prisma.bookChapter.findUnique({ where: { id: targetChapter.id }, select: { id: true, content: true, genStage: true }, }); const savedLen = (verify?.content || '').length; if (savedLen > 0) { console.log(`[PerChapter] 第${chapter.number}章内容保存验证 ✅: 写入${contentLen}字, 读取${savedLen}字`); } else { console.warn(`[PerChapter] 第${chapter.number}章内容保存验证 ⚠️: 写入${contentLen}字但读取为空,重试...`); // 重试一次 await bookStore.updateChapterById(targetChapter.id, { content: parsed.content }); const retryVerify = await prisma.bookChapter.findUnique({ where: { id: targetChapter.id }, select: { content: true }, }); console.log(`[PerChapter] 第${chapter.number}章重试后: ${(retryVerify?.content || '').length}字`); } } else { console.error(`[PerChapter] 第${chapter.number}章未找到 target 记录`); } completedChapters.push(chapter.number); console.log(`[PerChapter] 第${chapter.number}章「${chapter.title}」完成`); } catch (err) { console.error(`[PerChapter] 第${chapter.number}章失败:`, err); failedChapters.push(chapter.number); } } if (failedChapters.length === 0) { await bookStore.update(state.bookId, { progress: PROGRESS.CONTENT_END }); console.log(`[PerChapter] 全部完成,共${completedChapters.length}章`); } else { await bookStore.update(state.bookId, { genStage: 'failed', failedStage: 'content_generating', }); console.warn(`[PerChapter] ${completedChapters.length}章成功,${failedChapters.length}章失败`); } return { progress: PROGRESS.CONTENT_END, finished: failedChapters.length === 0 }; } catch (error) { console.error('[PerChapter] 处理失败:', error); await bookStore.update(state.bookId, { genStage: 'failed', failedStage: 'content_generating', errorMsg: error instanceof Error ? error.message : '逐章处理失败', }); return { error: error instanceof Error ? error.message : '失败', finished: true }; } } /** * 解析单章 AI 返回 */ function parseChapterContent(response: string): { content: string; wordCount: number } | null { try { let cleaned = response.trim(); const thinkEnd = cleaned.indexOf('</think>'); if (thinkEnd !== -1) cleaned = cleaned.substring(thinkEnd + 8).trim(); const match = cleaned.match(/\{[\s\S]*\}/); if (!match) { // 如果不是 JSON,可能是纯文本,直接当内容 return { content: cleaned, wordCount: cleaned.length }; } const data = JSON.parse(match[0]); return { content: data.content || cleaned, wordCount: data.wordCount || 0, }; } catch { // fallback: 把整个响应当内容 return { content: response, wordCount: response.length }; } } /** * 根据正文中的标题层级创建节/小节记录(仅索引,不存储内容) */ async function createInternalStructure( bookIdNum: number, chapterId: number, content: string, genLevel: number, chapterTitle: string ): Promise<void> { // 用正则提取 ### 和 #### 标题 const sectionRegex = /###\s+(\d+\.?\d*)\s+(.+)/g; const subsectionRegex = /####\s+(\d+\.?\d*)\s+(.+)/g; let match: RegExpExecArray | null; let sectionNumber = 0; while ((match = sectionRegex.exec(content)) !== null) { sectionNumber++; const sectionTitle = match[2].trim(); if (genLevel >= 3) { // 创建节记录,再提取该节下的小节 const sectionId = await bookStore.createChapterItem(bookIdNum, { number: sectionNumber, title: sectionTitle, summary: '', keyPoints: [], estimatedWords: 500, }, chapterId, 2); // 在该节范围内找 #### 标题 const sectionStart = match.index; const nextSectionMatch = sectionRegex.exec(content); const sectionEnd = nextSectionMatch ? nextSectionMatch.index : content.length; // 重置 regex 位置 sectionRegex.lastIndex = match.index + match[0].length; const sectionContent = content.substring(sectionStart, sectionEnd); let subMatch: RegExpExecArray | null; let subNumber = 0; const subRegex = /####\s+(\d+\.?\d*)\s+(.+)/g; while ((subMatch = subRegex.exec(sectionContent)) !== null) { subNumber++; await bookStore.createChapterItem(bookIdNum, { number: subNumber, title: subMatch[2].trim(), summary: '', keyPoints: [], estimatedWords: 300, }, sectionId, 3); } } else { // genLevel=2,直接创建节,不拆小节 await bookStore.createChapterItem(bookIdNum, { number: sectionNumber, title: sectionTitle, summary: '', keyPoints: [], estimatedWords: 800, }, chapterId, 2); } } } |