# All files / modules/book-generator/nodes content.node.ts

0% Statements 0/450
0% Branches 0/1
0% Functions 0/1
0% Lines 0/450

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
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    475
    476
    477
    478
    479
    480
    481
    482
    483
    484
    485
    486
    487
    488
    489
    490
    491
    492
    493
    494
    495
    496
    497
    498
    499
    500
    501
    502
    503
    504
    505
    506
    507
    508
    509
    510
    511
    512
    513
    514
    515
    516
    517
    518
    519
    520
    521
    522
    523
    524
    525
    526
    527
    528
    529
    530
    531
    532
    533
    534
    535
    536
    537
    538
    539
    540
    541
    542
    543
    544
    545
    546
    547
    548
    549
    550
    551
    552
    553
    554
    555
    556
    557
    558
    559
    560
    561
    562
    563
    564
    565
    566
    567
    568
    569
    570
    571
    572
    573
    574
    575
    576
    577
    578
    579
    580
    581
    582
    583
    584
    585
    586
    587
    588
    589
    590
    591
    592
    593
    594
    595
    596
    597
    598
    599
    600
    601
    602
    603
    604
    605
    606
    607
    608
    609
    610
    611
    612
    613
    614
    615
    616
    617
    618
    619
    620
    621
    622
    623
    624
    625
    626
    627
    628
    629
    630
    631
    632
    633
    634
    635
    636
    637
    638
    639
    640
    641
    642
    643
    644
    645
    646
    647
    648
    649
    650
    651
    652
    653
    654
    655
    656
    657
    658

     /**
     * 内容生成节点
     * 为所有叶节点（没有子节点的节点）生成实际内容
     */

    import { GraphState } from '../graph';
    import { bookStore } from '../book-generator.store';
    import { prisma } from '../../../models';
    import { createBookTools } from '../../../services/llm/book-tools';
    import { checkQuotaForWords, markGenerationInterrupted, getGeneratedWordCount } from '../../subscription/subscription.service';
    import { PROGRESS, countWords } from '../utils';
    import { advanceChapter, regenerateChapter } from '../stage-manager';
    import { getBookWordLimit, getWordUpperLimit } from '../book-type-config';
    import { AsyncPool } from '../utils/async-pool';
    import { cleanThinkingText, truncateAtBoundary } from '../utils/content-cleaner';
    import { safeTransitionChapter } from '../stage-manager';
    import { BookConsistencyTracker } from '../utils/content-consistency';
    import { withGlobalLLMConcurrency, getBookConcurrency } from '../utils/llm-concurrency';

    /** 全局术语一致性跟踪器（单例，跨整本书的章节共享） */
    const globalConsistencyTracker = new BookConsistencyTracker();

    /**
     * 为被截断的内容补一个自然结尾句，
     * 避免截断后看起来像没写完。
     */
    function appendNaturalClosure(content: string): string {
      if (!content || content.length < 50) return content;
      const trimmed = content.trimEnd();
      // 已有完整结尾标点的不补
      const naturalEndings = /[。！？.!?）」"”']\s*$/;
      if (naturalEndings.test(trimmed)) return trimmed;

      // 从内容末尾提取关键词，生成一个简短收尾句
      const lastSentences = trimmed.split(/[。！？.!?]/).filter(Boolean);
      const lastTopic = lastSentences.length > 0
        ? lastSentences[lastSentences.length - 1].substring(0, 30).trim()
        : '';

      if (lastTopic.length > 3) {
        return trimmed + `。以上就是关于${lastTopic}的讨论。`;
      }
      return trimmed + '。以上就是本章的主要内容。';
    }

    /**
     * 安全更新父章节状态：仅在父节点下所有子节点都已完成时，才推进父节点状态。
     * 替代原先的 updateMany 批量操作，避免失败子节点污染已完成父节点状态。
     */
    async function safelyCompleteParentChapters(bookIdNum: number): Promise<void> {
      const parents = await prisma.bookChapter.findMany({
        where: { bookId: bookIdNum, level: { in: [1, 2] } },
        select: { id: true, genStage: true },
      });

      const allChildren = await prisma.bookChapter.findMany({
        where: { bookId: bookIdNum, parentId: { in: parents.map(p => p.id) } },
        select: { id: true, genStage: true, parentId: true },
      });

      // 按 parentId 分组
      const childrenByParent = new Map<number, typeof allChildren>();
      for (const child of allChildren) {
        if (!child.parentId) continue;
        if (!childrenByParent.has(child.parentId)) childrenByParent.set(child.parentId, []);
        childrenByParent.get(child.parentId)!.push(child);
      }

      for (const parent of parents) {
        const children = childrenByParent.get(parent.id) || [];
        if (children.length === 0) continue; // 无子节点（单层大纲），跳过

        const allChildrenDone = children.every(
          c => c.genStage === 'content_completed' || c.genStage === 'audio_generating'
            || c.genStage === 'audio_completed' || c.genStage === 'video_generating'
            || c.genStage === 'video_completed'
        );

        if (allChildrenDone && parent.genStage !== 'content_completed') {
          try {
            await safeTransitionChapter(parent.id, parent.genStage as any, 'content_completed');
          } catch (err: any) {
            console.warn(`[Content] 父节点 #${parent.id} 状态转移失败:`, err.message);
          }
        }
      }
    }

    /**
     * 内容生成节点
     * 为所有叶节点（没有子节点的节点）生成实际内容
     */
    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 allChapters = await prisma.bookChapter.findMany({
        where: { bookId: bookIdNum },
        orderBy: { number: 'asc' }
      });

      // 找出所有叶节点（没有子节点的节点）
      const allParentIds = new Set(allChapters.map(c => c.parentId));
      const leafNodes = allChapters.filter(c => !allParentIds.has(c.id));

      // 过滤出未完成内容的叶节点
      const targetsToGenerate = leafNodes.filter((n: any) => n.genStage !== 'content_completed' && !n.content);
      const alreadyCompleted = leafNodes.length - targetsToGenerate.length;

      if (alreadyCompleted > 0) {
        console.log(`[LangGraph] ✅ 跳过 ${alreadyCompleted} 个已生成内容的目标`);
      }

      if (targetsToGenerate.length === 0) {
        console.log('[LangGraph] ✅ 所有内容已完成，跳过内容生成（音频由 AudioScanner 统一负责）');
        return { finished: true, progress: PROGRESS.CONTENT_END };
      }

      // 正常生成内容流程
      console.log(`[LangGraph] 共 ${leafNodes.length} 个叶节点，${targetsToGenerate.length} 个待生成`);

      // 构建父节点映射（用于长篇书籍）
      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}`);

      const totalTargets = targetsToGenerate.length;
      let completedCount = 0;

      for (const target of targetsToGenerate) {
        // 查找父节点信息
        const parentSection = sectionMap.get(target.parentId || 0);
        const parentChapter = parentSection ? chapterMap.get(parentSection.parentId || 0) : null;

        const chapterTitle = parentChapter?.title || '未知章';
        const sectionTitle = parentSection?.title || '未知节';
        const chapterSummary = parentChapter?.summary || '';
        const sectionSummary = parentSection?.summary || '';

        // 额度监控
        try {
          const quotaCheck = await checkQuotaForWords(book.userId || 1, currentWordCount);
          if (!quotaCheck.sufficient) {
            console.warn(`[LangGraph] ⚠️ 额度不足，中断生成`);
            await markGenerationInterrupted(state.bookId, target.number - 1, currentWordCount);
            return {
              currentChapter: target.number,
              progress: Math.round((completedCount / totalTargets) * 80) + 15,
              error: `额度不足中断`
            };
          }
        } catch (quotaErr) {
          console.warn(`[LangGraph] 额度检查失败，继续生成`);
        }

        // 字数校验：累计字数达到规模值的120%时停止生成（AI自行理解±20%浮动）
        const scaleWords = parseInt(state.bookScale);
        if (scaleWords && currentWordCount >= getWordUpperLimit(scaleWords)) {
          console.warn(`[LangGraph] ⚠️ 累计字数 ${currentWordCount} 已达规模上限 ${getWordUpperLimit(scaleWords)}（基数${scaleWords}×120%），停止生成`);
          return {
            currentChapter: target.number,
            progress: PROGRESS.CONTENT_END,
            error: `已达规模上限，约${Math.round(getWordUpperLimit(scaleWords) / 10000)}万字`
          };
        }

        // 根据是否有父章节判断是短文还是长篇
        const isShortArticle = !parentSection && target.level === 1;
        // 注：messages 由 generateSingleChapterContent 内部构建，此处只记录日志
        if (isShortArticle) {
          console.log(`[LangGraph] 生成短文「${target.title}」`);
        } else {
          console.log(`[LangGraph] 生成${chapterTitle} - ${sectionTitle} - ${target.title}`);
        }

        try {
          // ===== 直接生成内容（不使用队列，由 LangGraph 内部控制） =====
          const existing = await prisma.bookChapter.findUnique({
            where: { id: target.id },
            select: { content: true, wordCount: true, genStage: true },
          });
          if (existing?.content && existing.genStage === 'content_completed') {
            console.log(`[LangGraph] 「${target.title}」已有完成内容，复用`);
            currentWordCount += existing.wordCount || countWords(existing.content);
            completedCount++;
          } else {
            console.log(`[LangGraph] 「${target.title}」开始生成...`);
            const MAX_RETRIES = 3;
            let lastError: Error | null = null;
            for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
              try {
                if (attempt > 0) {
                  const delay = Math.min(3000 * Math.pow(2, attempt - 1) + Math.random() * 2000, 30000);
                  console.log(`[LangGraph] 「${target.title}」第${attempt + 1}次重试，等待${(delay / 1000).toFixed(1)}s...`);
                  await new Promise(resolve => setTimeout(resolve, delay));
                }
                await withGlobalLLMConcurrency(() => bookStore.generateSingleChapterContent(state.bookId, target.id));
                lastError = null;
                break;
              } catch (err: any) {
                lastError = err;
                console.error(`[LangGraph] 「${target.title}」第${attempt + 1}次失败:`, err.message);
              }
            }
            if (lastError) throw lastError;
            completedCount++;
          }

          // 重新读取章节（队列处理器已更新）
          const chapter = await prisma.bookChapter.findUnique({
            where: { id: target.id },
            select: { content: true, wordCount: true },
          });
          if (!chapter?.content) throw new Error('内容生成结果为空');

          let content = chapter.content;
          let wordCount = chapter.wordCount || countWords(content);
          currentWordCount += wordCount;

          // ========== 三层安全防护 ==========

          // Layer 2: 章节预算偏差检测 — 按句截断 + 补结尾句
          const estimated = target.estimatedWords || 500;
          if (estimated > 0 && wordCount > estimated * 3) {
            console.warn(`[Security] ⚠️ 章节「${target.title}」字数异常: ${wordCount} > 预算 ${estimated}×3，截断处理`);
            const budgetChars = Math.round(estimated * 2.5);
            content = truncateAtBoundary(content, budgetChars, 'sentence');
            content = appendNaturalClosure(content);
            wordCount = countWords(content);
            await bookStore.updateChapterById(target.id, { content, wordCount });
          }

          // Layer 3: 全书累计字数上限
          const bookWordLimit = getBookWordLimit();
          if (currentWordCount > bookWordLimit) {
            console.warn(`[Security] ⚠️ 全书字数超限: ${currentWordCount} > ${bookWordLimit}，中断生成`);
            await markGenerationInterrupted(state.bookId, target.number, currentWordCount);
            return {
              currentChapter: target.number,
              progress: Math.round((completedCount / totalTargets) * 80) + 15,
              error: `全书字数超限（${currentWordCount} > ${bookWordLimit}）`
            };
          }

          // 音频生成由 AudioScanner 统一负责，不在此处 fire-and-forget
          // 配额消耗已移至 TTS 完成回调（processTtsTask onComplete），按实际音频时长扣费

          const progress = Math.round((completedCount / totalTargets) * (PROGRESS.CONTENT_END - PROGRESS.CONTENT_START)) + PROGRESS.CONTENT_START;
          await bookStore.update(state.bookId, { progress });
          console.log(`[LangGraph] 「${target.title}」内容完成 (${completedCount}/${totalTargets})，累计${currentWordCount}字，进度${progress}%`);

        } catch (error) {
          const errorMsg = error instanceof Error ? error.message : '失败';
          await bookStore.updateChapterById(target.id, {
            contentError: errorMsg
          });
          await regenerateChapter(target.id, 'failed');
          failedChapters.push(target.number);
          console.error(`[LangGraph] 「${target.title}」内容失败:`, errorMsg);
        }
      }

      // 安全更新父节点状态（逐条验证子节点，不再用 updateMany 盲改）
      await safelyCompleteParentChapters(bookIdNum);

      return {
        currentChapter: leafNodes.length,
        progress: PROGRESS.CONTENT_END,
        error: failedChapters.length > 0 ? `小节${failedChapters.join(',')}失败` : undefined
      };
    }

    // ============ 并行内容生成节点（新策略专用） ============

    /** 默认并发数（可通过环境变量 LLM_BOOK_CONCURRENCY 配置） */
    const PARALLEL_CONCURRENCY = getBookConcurrency();

    /**
     * 解析 crossReferences，按依赖层级分组
     * 组0 = 无依赖的节点（可最先并行）
     * 组1 = 依赖组0的节点
     * 组2 = 依赖组0/组1的节点
     * 依此类推
     */
    function groupByDependencyLayer(
      targets: any[],
      crossReferences: Record<string, { dependsOn: string[]; usedBy: string[] }> | undefined
    ): { layer: number; nodes: any[] }[] {
      if (!crossReferences || Object.keys(crossReferences).length === 0) {
        // 无 crossReferences，全部放第一层（最大并行）
        return [{ layer: 0, nodes: targets }];
      }

      // 建立 number → 节点映射
      const nodeByNumber = new Map<number, any>();
      targets.forEach(t => nodeByNumber.set(t.number, t));

      // 计算每个节点的入度（有多少依赖）
      interface NodeDegree { node: any; dependsOn: number[]; layer: number; }
      const degrees: NodeDegree[] = targets.map(t => ({
        node: t,
        dependsOn: [],
        layer: 0,
      }));

      // 解析依赖关系
      for (const [refKey, ref] of Object.entries(crossReferences)) {
        // refKey 格式: "ch3" 或 "chapter3"
        const match = refKey.match(/ch(?:apter)?(\d+)/i);
        if (!match) continue;
        const num = parseInt(match[1]);
        const node = degrees.find(d => d.node.number === num);
        if (!node) continue;

        // 解析 dependsOn
        const depNums: number[] = [];
        for (const dep of (ref.dependsOn || [])) {
          const depMatch = dep.match(/ch(?:apter)?(\d+)/i);
          if (depMatch) depNums.push(parseInt(depMatch[1]));
        }
        node.dependsOn = depNums;
      }

      // 拓扑分层：迭代找出每层的无依赖节点
      const layers: any[][] = [];
      const assigned = new Set<number>();
      let changed = true;
      while (changed) {
        changed = false;
        const currentLayer: any[] = [];
        for (const d of degrees) {
          if (assigned.has(d.node.number)) continue;
          // 检查所有依赖是否已分配
          const allDepsAssigned = d.dependsOn.every(dep => assigned.has(dep));
          if (allDepsAssigned || d.dependsOn.length === 0) {
            currentLayer.push(d.node);
            assigned.add(d.node.number);
            changed = true;
          }
        }
        if (currentLayer.length > 0) layers.push(currentLayer);
      }

      // 未分配节点（孤立的循环依赖等）放入最后一层
      const unassigned = targets.filter(t => !assigned.has(t.number));
      if (unassigned.length > 0) layers.push(unassigned);

      return layers.map((nodes, i) => ({ layer: i, nodes }));
    }

    /**
     * 为单个叶节点生成内容的函数（供并行调用）
     */
    async function generateSingleNodeContent(
      target: any,
      context: {
        bookId: string;
        bookIdNum: number;
        topic: string;
        chapterMap: Map<number, any>;
        sectionMap: Map<number, any>;
        bookTools: any;
        book: any;
        bookDescription: string;
        writingStyle: string;
      }
    ): Promise<{ success: boolean; wordCount: number; error?: string }> {
      const { bookId, bookIdNum, topic, chapterMap, sectionMap, bookTools, book, bookDescription, writingStyle } = context;

      // 查找父节点信息
      const parentSection = sectionMap.get(target.parentId || 0);
      const parentChapter = parentSection ? chapterMap.get(parentSection.parentId || 0) : null;

      const chapterTitle = parentChapter?.title || '未知章';
      const sectionTitle = parentSection?.title || '未知节';
      const chapterSummary = parentChapter?.summary || '';
      const sectionSummary = parentSection?.summary || '';

      // 判断是短文还是长篇
      const isShortArticle = !parentSection && target.level === 1;
      // 注：messages 由 generateSingleChapterContent 内部构建，此处只记录日志
      if (isShortArticle) {
        console.log(`[ParallelContent] 生成短文「${target.title}」`);
      } else {
        console.log(`[ParallelContent] 生成${chapterTitle} - ${sectionTitle} - ${target.title}`);
      }

      try {
        // ===== 直接生成内容（不使用队列，由 AsyncPool 并发生成） =====
        const existing = await prisma.bookChapter.findUnique({
          where: { id: target.id },
          select: { content: true, wordCount: true, genStage: true },
        });
        if (existing?.content && existing.genStage === 'content_completed') {
          return { success: true, wordCount: existing.wordCount || countWords(existing.content) };
        }

        const MAX_RETRIES = 3;
        let lastError: Error | null = null;
        for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
          try {
            if (attempt > 0) {
              const delay = Math.min(3000 * Math.pow(2, attempt - 1) + Math.random() * 2000, 30000);
              console.log(`[ParallelContent] 「${target.title}」第${attempt + 1}次重试，等待${(delay / 1000).toFixed(1)}s...`);
              await new Promise(resolve => setTimeout(resolve, delay));
            }
            // 全局并发控制：LLM 调用走信号量，避免多书同时打爆 API 限流
            await withGlobalLLMConcurrency(() => bookStore.generateSingleChapterContent(bookId, target.id));
            lastError = null;
            break;
          } catch (err: any) {
            lastError = err;
            console.error(`[ParallelContent] 「${target.title}」第${attempt + 1}次失败:`, err.message);
          }
        }
        if (lastError) throw lastError;

        // 重新读取（generateSingleChapterContent 已更新 DB）
        const chapter = await prisma.bookChapter.findUnique({
          where: { id: target.id },
          select: { content: true },
        });
        let content = chapter?.content || '';
        let wordCount = countWords(content);

        // 章节预算偏差检测 — 智能截断（按句截断 + 补结尾句）
        const estimated = target.estimatedWords || 500;
        if (estimated > 0 && wordCount > estimated * 3) {
          console.warn(`[Security] 「${target.title}」字数异常: ${wordCount} > ${estimated}×3，截断`);
          const budgetChars = Math.round(estimated * 2.5);
          content = truncateAtBoundary(content, budgetChars, 'sentence');
          // 补一个自然结尾句，避免截断后看起来像没写完
          content = appendNaturalClosure(content);
          wordCount = countWords(content);
          await bookStore.updateChapterById(target.id, { content, wordCount });
        }

        // ===== 事中一致性跟踪：注册术语到全局跟踪器 =====
        if (content && content.length > 50) {
          try {
            globalConsistencyTracker.registerChapter(
              target.number,
              target.title || '',
              content
            );
          } catch (trackErr) {
            // 跟踪失败不阻塞内容生成
            console.warn(`[ParallelContent] 一致性跟踪失败:`, trackErr);
          }
        }

        // 配额消耗已移至 TTS 完成回调，不在内容生成阶段扣除
        // 音频生成由 AudioScanner 统一负责，不在此处 fire-and-forget

        return { success: true, wordCount };
      } catch (error) {
        const errorMsg = error instanceof Error ? error.message : '失败';
        await bookStore.updateChapterById(target.id, { contentError: errorMsg });
        await regenerateChapter(target.id, 'failed');
        console.error(`[ParallelContent] 「${target.title}」失败:`, errorMsg);
        return { success: false, wordCount: 0, error: errorMsg };
      }
    }

    /**
     * 并行内容生成节点
     *
     * 使用 AsyncPool 并发生成所有叶节点的内容。
     * 根据 deepPlan 中的 crossReferences 做拓扑排序，
     * 有依赖的章节先生成，被依赖的章节后生成。
     *
     * 解决 issue #2（内容生成纯串行）
     */
    export async function writeChaptersParallelNode(state: typeof GraphState.State): Promise<Partial<typeof GraphState.State>> {
      console.log('[ParallelContent] 并行生成章节内容, bookId:', state.bookId);

      const book = await bookStore.getById(state.bookId);
      if (!book || !book.outline) {
        console.log('[ParallelContent] 无大纲或书籍，跳过');
        return { finished: true, progress: PROGRESS.CONTENT_END };
      }

      const bookIdNum = parseInt(state.bookId);

      // 查找所有章节
      const allChapters = await prisma.bookChapter.findMany({
        where: { bookId: bookIdNum },
        orderBy: { number: 'asc' }
      });

      // 找出所有叶节点
      const allParentIds = new Set(allChapters.map(c => c.parentId));
      const leafNodes = allChapters.filter(c => !allParentIds.has(c.id));

      // 过滤出未完成内容的目标
      const targetsToGenerate = leafNodes.filter((n: any) => n.genStage !== 'content_completed' && !n.content);

      if (targetsToGenerate.length === 0) {
        console.log('[ParallelContent] 所有内容已完成（音频由 AudioScanner 统一负责）');
        return { finished: true, progress: PROGRESS.CONTENT_END };
      }

      console.log(`[ParallelContent] ${targetsToGenerate.length} 个叶节点待生成，并发数=${PARALLEL_CONCURRENCY}`);

      // 构建父节点映射
      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 bookTools = createBookTools(state.bookId, bookStore);

      // 解析 crossReferences 做拓扑分组
      let crossReferences: Record<string, { dependsOn: string[]; usedBy: string[] }> = {};
      if (state.bookPlan) {
        try {
          const plan = JSON.parse(state.bookPlan);
          crossReferences = plan.crossReferences || {};
        } catch { /* ignore */ }
      }

      // 按依赖层级分组
      const layers = groupByDependencyLayer(targetsToGenerate, crossReferences);
      console.log(`[ParallelContent] 拓扑分层: ${layers.length} 层`);
      layers.forEach((l, i) => {
        console.log(`  层${i}: [${l.nodes.map(n => n.number).join(', ')}]`);
      });

      // 分层执行：每层内并行，层间串行（依赖满足后下一层才能开始）
      const context = {
        bookId: state.bookId,
        bookIdNum,
        topic: state.topic,
        chapterMap,
        sectionMap,
        bookTools,
        book,
        bookDescription: book.description || '',
        writingStyle: book.style || '',
      };

      let totalSucceeded = 0;
      let totalFailed = 0;
      let totalWords = 0;

      for (const { layer, nodes } of layers) {
        if (nodes.length === 0) continue;
        console.log(`[ParallelContent] 执行层${layer}（${nodes.length}个节点，并发=${PARALLEL_CONCURRENCY}）`);

        // ===== 实时字数校验：在每层开始前检查是否已超限 =====
        const currentTotalWords = await getGeneratedWordCount(state.bookId);
        const scaleWords = parseInt(state.bookScale);
        if (scaleWords && currentTotalWords >= getWordUpperLimit(scaleWords)) {
          console.warn(`[ParallelContent] ⚠️ 累计字数 ${currentTotalWords} 已达规模上限 ${getWordUpperLimit(scaleWords)}，终止后续层`);
          break;
        }
        const bookWordLimit = getBookWordLimit();
        if (currentTotalWords > bookWordLimit) {
          console.warn(`[ParallelContent] ⚠️ 全书字数超限 ${currentTotalWords} > ${bookWordLimit}，终止后续层`);
          break;
        }

        const tasks = nodes.map(target => ({
          fn: () => generateSingleNodeContent(target, context),
        }));

        const pool = new AsyncPool(PARALLEL_CONCURRENCY);
        const results = await pool.runAll(tasks);

        const succeeded = results.filter(r => r.success).length;
        const failed = results.filter(r => !r.success).length;
        totalSucceeded += succeeded;
        totalFailed += failed;
        totalWords += results.reduce((sum, r) => sum + r.wordCount, 0);

        console.log(`[ParallelContent] 层${layer}完成: ${succeeded} 成功, ${failed} 失败`);

        // ===== 失败比例超阈值终止 =====
        const totalProcessed = totalSucceeded + totalFailed;
        const FAILURE_THRESHOLD = 0.3; // 30% 失败率
        if (totalProcessed >= 3 && totalFailed / totalProcessed > FAILURE_THRESHOLD) {
          console.warn(`[ParallelContent] ⚠️ 失败率 ${((totalFailed / totalProcessed) * 100).toFixed(1)}% 超过阈值 ${(FAILURE_THRESHOLD * 100).toFixed(0)}%，终止后续层`);
          break;
        }
      }

      console.log(`[ParallelContent] 总计: ${totalSucceeded} 成功, ${totalFailed} 失败, 总字数=${totalWords}`);

      // 安全更新父节点状态（逐条验证，不再用 updateMany 盲改）
      await safelyCompleteParentChapters(bookIdNum);

      // 检查是否有配额/字数问题
      const currentWordCount = await getGeneratedWordCount(state.bookId);
      const scaleWords = parseInt(state.bookScale);
      const interrupts: string[] = [];

      if (scaleWords && currentWordCount >= getWordUpperLimit(scaleWords)) {
        interrupts.push(`已达规模上限（${currentWordCount}字）`);
      }

      const bookWordLimit = getBookWordLimit();
      if (currentWordCount > bookWordLimit) {
        interrupts.push(`全书字数超限（${currentWordCount} > ${bookWordLimit}）`);
      }

      // ===== 事中一致性报告（Issue 4）=====
      if (totalSucceeded > 0) {
        const report = globalConsistencyTracker.generateReport();
        if (report.hasConflicts) {
          console.warn(`[ParallelContent] ⚠️ 一致性检查发现 ${report.issues.length} 个问题：`);
          report.issues.forEach(issue => {
            console.warn(`  [${issue.severity}] ${issue.type}: ${issue.description}`);
          });
          // 将问题追加到 error 字段，前端可展示
          if (report.issues.filter(i => i.severity === 'high').length > 0) {
            interrupts.push(`术语一致性：${report.issues.filter(i => i.severity === 'high').length}个冲突`);
          }
        } else {
          console.log(`[ParallelContent] ✅ 术语一致性检查通过（${report.terms.length}个术语无冲突）`);
        }
      }

      return {
        currentChapter: leafNodes.length,
        progress: PROGRESS.CONTENT_END,
        error: totalFailed > 0
          ? `${totalFailed}个节点失败` + (interrupts.length > 0 ? `; ${interrupts.join('; ')}` : '')
          : (interrupts.length > 0 ? interrupts.join('; ') : undefined),
      };
    }