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

refactor: 拆分JSON解析器 - 提取parsers/*.ts

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

+ 75 - 0
server/src/modules/book-generator/parsers/outline.parser.ts

@@ -0,0 +1,75 @@
+/**
+ * 大纲解析器
+ * 容错解析LLM返回的大纲JSON
+ */
+
+export interface OutlineData {
+  mainTheme: string;
+  structureLogic: string;
+  chapters: Array<{
+    number: number;
+    title: string;
+    summary: string;
+    keyPoints: string[];
+    estimatedWords: number;
+  }>;
+}
+
+export function parseOutline(jsonStr: string): OutlineData | null {
+  if (!jsonStr || typeof jsonStr !== 'string') {
+    console.error('[OutlineParser] 输入为空或非字符串');
+    return null;
+  }
+
+  try {
+    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;
+  }
+}

+ 89 - 0
server/src/modules/book-generator/parsers/section.parser.ts

@@ -0,0 +1,89 @@
+/**
+ * 节解析器
+ * 容错解析LLM返回的节大纲JSON
+ */
+
+export 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;
+  }
+}

+ 108 - 0
server/src/modules/book-generator/parsers/subsection.parser.ts

@@ -0,0 +1,108 @@
+/**
+ * 小节解析器
+ * 容错解析LLM返回的小节大纲JSON
+ */
+
+export 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;
+  }
+}