All files / modules/book-generator/utils outline-quality.ts

0% Statements 0/117
100% Branches 1/1
100% Functions 1/1
0% Lines 0/117

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                                                                                                                                                                                                                                                                                                                                                         
/**
 * 大纲质量评估器
 *
 * 解决 issue #10(没有大纲质量评估)
 * 在大纲生成后对其质量做结构化检查,发现问题可触发重新生成或人工审核。
 */
 
export interface OutlineQualityResult {
  passed: boolean;
  score: number; // 0-100
  warnings: OutlineWarning[];
}
 
export interface OutlineWarning {
  type: 'duplicate_title' | 'too_many_chapters' | 'too_few_chapters' | 'empty_summary' | 'empty_keypoints' | 'granularity_issue';
  message: string;
  chapterNumber?: number;
  severity: 'low' | 'medium' | 'high';
}
 
interface OutlineChapter {
  number: number;
  title: string;
  summary?: string;
  keyPoints?: string[];
  estimatedWords?: number;
  sections?: any[];
}
 
/**
 * 评估大纲质量
 *
 * @param chapters 大纲章节列表
 * @param scaleConfig 规模配置(含 targetChapters, chapterWords 等)
 * @param genLevel 大纲层级
 */
export function evaluateOutlineQuality(
  chapters: OutlineChapter[],
  scaleConfig: { chapters?: number; chapterWords?: number; totalWords?: number },
  genLevel: number = 2
): OutlineQualityResult {
  const warnings: OutlineWarning[] = [];
  let score = 100;
 
  if (!chapters || chapters.length === 0) {
    return { passed: false, score: 0, warnings: [{ type: 'too_few_chapters', message: '大纲章节数为空', severity: 'high' }] };
  }
 
  // 1. 检查章节标题重复
  const titleMap = new Map<string, number[]>();
  chapters.forEach((ch) => {
    const normalized = ch.title?.trim().toLowerCase() || '';
    if (normalized) {
      if (!titleMap.has(normalized)) titleMap.set(normalized, []);
      titleMap.get(normalized)!.push(ch.number);
    }
  });
 
  titleMap.forEach((numbers, title) => {
    if (numbers.length > 1) {
      warnings.push({
        type: 'duplicate_title',
        message: `章节标题重复: "${title}" 出现在第 ${numbers.join('、')} 章`,
        chapterNumber: numbers[0],
        severity: 'high',
      });
      score -= 15 * (numbers.length - 1);
    }
  });
 
  // 2. 检查章节数范围
  if (scaleConfig.chapters) {
    const minChapters = Math.floor(scaleConfig.chapters * 0.8);
    const maxChapters = Math.ceil(scaleConfig.chapters * 1.2);
 
    if (chapters.length > maxChapters) {
      warnings.push({
        type: 'too_many_chapters',
        message: `章节数 ${chapters.length} 超出推荐范围(${minChapters}-${maxChapters})`,
        severity: 'medium',
      });
      score -= 10;
    } else if (chapters.length < minChapters) {
      warnings.push({
        type: 'too_few_chapters',
        message: `章节数 ${chapters.length} 少于推荐范围(${minChapters}-${maxChapters})`,
        severity: 'medium',
      });
      score -= 10;
    }
  }
 
  // 3. 检查每章是否有摘要和知识点
  let emptySummaryCount = 0;
  let emptyKeyPointsCount = 0;
 
  chapters.forEach((ch) => {
    if (!ch.summary || ch.summary.trim().length < 10) {
      emptySummaryCount++;
    }
    if (!ch.keyPoints || ch.keyPoints.length === 0) {
      emptyKeyPointsCount++;
    }
  });
 
  if (emptySummaryCount > 0) {
    warnings.push({
      type: 'empty_summary',
      message: `${emptySummaryCount}/${chapters.length} 个章节缺少摘要`,
      severity: emptySummaryCount > chapters.length * 0.3 ? 'high' : 'medium',
    });
    score -= emptySummaryCount * 3;
  }
 
  if (emptyKeyPointsCount > 0) {
    warnings.push({
      type: 'empty_keypoints',
      message: `${emptyKeyPointsCount}/${chapters.length} 个章节缺少知识点`,
      severity: emptyKeyPointsCount > chapters.length * 0.3 ? 'high' : 'medium',
    });
    score -= emptyKeyPointsCount * 3;
  }
 
  // 4. 章节知识点粒度检查(太少的要报警)
  chapters.forEach((ch) => {
    if (ch.keyPoints && ch.keyPoints.length > 0 && ch.keyPoints.length < 2) {
      warnings.push({
        type: 'granularity_issue',
        message: `第${ch.number}章知识点过少(${ch.keyPoints.length}个),可能粒度太粗`,
        chapterNumber: ch.number,
        severity: 'low',
      });
      score -= 2;
    }
    if (ch.keyPoints && ch.keyPoints.length > 8) {
      warnings.push({
        type: 'granularity_issue',
        message: `第${ch.number}章知识点过多(${ch.keyPoints.length}个),可能粒度太细`,
        chapterNumber: ch.number,
        severity: 'low',
      });
      score -= 2;
    }
  });
 
  // 5. 对于 genLevel >= 2,检查节的结构
  if (genLevel >= 2) {
    let chaptersWithoutSections = 0;
    chapters.forEach((ch) => {
      if (!ch.sections || ch.sections.length === 0) {
        chaptersWithoutSections++;
      }
    });
    if (chaptersWithoutSections > 0) {
      warnings.push({
        type: 'granularity_issue',
        message: `${chaptersWithoutSections}/${chapters.length} 章缺少节结构`,
        severity: 'low',
      });
      score -= chaptersWithoutSections * 2;
    }
  }
 
  // 确保分数在 0-100 之间
  score = Math.max(0, Math.min(100, Math.round(score)));
 
  return {
    passed: score >= 60 && warnings.filter(w => w.severity === 'high').length === 0,
    score,
    warnings,
  };
}