Browse Source

refactor: 拆分提示词模板 - 提取prompts/templates.ts

MyFramework User 4 tháng trước cách đây
mục cha
commit
9a254b68fb

+ 19 - 0
server/check-books.js

@@ -0,0 +1,19 @@
+const { PrismaClient } = require('@prisma/client');
+const prisma = new PrismaClient();
+
+async function checkBooks() {
+  const books = await prisma.book.findMany({
+    orderBy: { id: 'desc' },
+    take: 3,
+    include: { _count: { select: { chapters: true } } }
+  });
+
+  console.log('最新3本书:');
+  books.forEach(b => {
+    console.log(`  id=${b.id}, title=${b.title}, status=${b.status}, chapters=${b._count.chapters}`);
+  });
+
+  await prisma.$disconnect();
+}
+
+checkBooks();

+ 12 - 0
server/clean-generating.js

@@ -0,0 +1,12 @@
+const { PrismaClient } = require('@prisma/client');
+const prisma = new PrismaClient();
+
+async function clean() {
+  const result = await prisma.book.deleteMany({
+    where: { status: 'generating' }
+  });
+  console.log(`✅ 已清理 ${result.count} 本生成中的书`);
+  await prisma.$disconnect();
+}
+
+clean();

+ 19 - 0
server/clear-redis-queue.js

@@ -0,0 +1,19 @@
+const Redis = require('ioredis');
+const redis = new Redis();
+
+async function clearQueue() {
+  console.log('清空Redis中的书籍生成队列...');
+  
+  // 删除所有bull队列相关key
+  const keys = await redis.keys('bull:book:generation:*');
+  if (keys.length > 0) {
+    await redis.del(...keys);
+    console.log(`✅ 已删除 ${keys.length} 个队列key`);
+  } else {
+    console.log('队列已空');
+  }
+  
+  await redis.quit();
+}
+
+clearQueue();

+ 173 - 0
server/src/modules/book-generator/prompts/templates.ts

@@ -0,0 +1,173 @@
+/**
+ * 提示词模板
+ * 所有静态提示词定义
+ */
+
+/**
+ * 大纲生成系统提示词
+ * OpenMAIC 模式:系统提示词详细,用户提示词简洁
+ */
+export 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
+    }
+  ]
+}`;
+
+/**
+ * 章节写作系统提示词(支持工具调用)
+ */
+export 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 格式说明`;
+
+/**
+ * 节大纲生成系统提示词
+ * 根据章的大纲,生成该章下各节的详细大纲
+ */
+export const SECTION_SYSTEM_PROMPT = `你是一位专业的图书策划编辑,擅长为书籍章节设计清晰的内部结构。
+
+## 你的职责
+根据给定的章主题和概述,设计该章下各节的详细大纲。
+
+## 输出格式
+必须返回 JSON,不要包含任何 markdown 代码块标记:
+{
+  "sections": [
+    {
+      "number": 1,
+      "title": "节标题",
+      "summary": "本节概述(1-2句话)",
+      "keyPoints": ["要点1", "要点2"],
+      "estimatedWords": 1500
+    }
+  ]
+}`;
+
+/**
+ * 小节大纲生成系统提示词
+ * 根据节的大纲,生成该节下各小节的详细大纲
+ */
+export const SUBSECTION_SYSTEM_PROMPT = `你是一位专业的图书策划编辑,擅长为书籍小节设计具体的知识点结构。
+
+## 你的职责
+根据给定的节主题和概述,设计该节下各小节的详细大纲。
+
+## 输出格式
+必须返回 JSON,不要包含任何 markdown 代码块标记:
+{
+  "subsections": [
+    {
+      "number": 1,
+      "title": "小节标题",
+      "summary": "本小节概述(1-2句话)",
+      "keyPoints": ["要点1", "要点2"],
+      "estimatedWords": 800
+    }
+  ]
+}`;
+
+/**
+ * 前言系统提示词
+ * 借鉴 OpenMAIC:详细定义角色 + 格式约束 + 质量要求
+ */
+export const FOREWORD_SYSTEM_PROMPT = `你是一位资深作家,擅长撰写引人入胜的书籍前言。
+
+## 你的职责
+为书籍撰写一篇精彩的前言,吸引读者继续阅读。
+
+## 质量要求
+1. 篇幅 300-500 字,语言流畅有感染力
+2. 开篇要有亮点,能抓住读者注意力(可用故事、名言、问题等切入)
+3. 简要介绍本书的主题、价值和特色,但不剧透核心内容
+4. 语气真诚、有热情,让读者感受到作者对主题的热爱
+5. 可以分享写作缘由或目标读者定位
+
+## 格式要求
+- 直接输出正文,不要加"前言"标题
+- 不要使用 markdown 格式标记(不加 #、**、- 等)
+- 不要在结尾写"希望读者..."之类的客套话
+- 直接开始叙述,第一句就要有吸引力`;
+
+/**
+ * 后记系统提示词
+ */
+export const AFTERWORD_SYSTEM_PROMPT = `你是一位资深作家,擅长撰写令人回味的书籍后记。
+
+## 你的职责
+为书籍撰写一篇有余韵的后记,让读者有所收获和思考。
+
+## 质量要求
+1. 篇幅 300-500 字,收尾有力
+2. 可以总结全书核心观点,但要用自己的话提炼而非重复
+3. 分享写作过程中的感悟、挑战或有趣发现
+4. 给读者留下思考空间或行动指引
+5. 语气真诚、谦逊,有深度但不说教
+
+## 格式要求
+- 直接输出正文,不要加"后记"标题
+- 不要使用 markdown 格式标记
+- 不要写"感谢读者"之类的套话
+- 结尾要有力量感,可以是金句、问题或开放性思考`;
+
+/**
+ * 小节内容写作提示词
+ */
+export 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 格式标记`;

+ 92 - 0
server/test-dual-status.js

@@ -0,0 +1,92 @@
+const { PrismaClient } = require('@prisma/client');
+const prisma = new PrismaClient();
+
+async function testStatus() {
+  console.log('=== 测试双阶段状态管理 ===\n');
+
+  // 查询最新的书籍
+  const book = await prisma.book.findFirst({
+    orderBy: { id: 'desc' },
+    include: {
+      chapters: {
+        orderBy: [{ level: 'asc' }, { number: 'asc' }]
+      }
+    }
+  });
+
+  if (!book) {
+    console.log('❌ 未找到测试书籍');
+    return;
+  }
+
+  console.log(`📖 书籍: ${book.title} (id=${book.id})`);
+  console.log(`状态: ${book.status}\n`);
+
+  // 统计各级节点
+  const level1Chapters = book.chapters.filter(c => c.level === 1);
+  const level2Chapters = book.chapters.filter(c => c.level === 2);
+  const level3Chapters = book.chapters.filter(c => c.level === 3);
+
+  console.log(`📊 统计:`);
+  console.log(`  章(level=1): ${level1Chapters.length} 个`);
+  console.log(`  节(level=2): ${level2Chapters.length} 个`);
+  console.log(`  小节(level=3): ${level3Chapters.length} 个\n`);
+
+  // 检查status字段(大纲状态)
+  console.log(`📝 大纲状态 (status):`);
+  const statusStats = {
+    pending: book.chapters.filter(c => c.status === 'pending').length,
+    completed: book.chapters.filter(c => c.status === 'completed').length,
+    failed: book.chapters.filter(c => c.status === 'failed').length,
+  };
+  console.log(`  completed: ${statusStats.completed}`);
+  console.log(`  pending: ${statusStats.pending}`);
+  console.log(`  failed: ${statusStats.failed}\n`);
+
+  // 检查contentStatus字段(内容状态)
+  console.log(`📄 内容状态 (contentStatus):`);
+  const contentStats = {
+    null: book.chapters.filter(c => c.contentStatus === null).length,
+    pending: book.chapters.filter(c => c.contentStatus === 'pending').length,
+    generating: book.chapters.filter(c => c.contentStatus === 'generating').length,
+    completed: book.chapters.filter(c => c.contentStatus === 'completed').length,
+    failed: book.chapters.filter(c => c.contentStatus === 'failed').length,
+  };
+  console.log(`  null (未开始): ${contentStats['null']}`);
+  console.log(`  pending: ${contentStats.pending}`);
+  console.log(`  generating: ${contentStats.generating}`);
+  console.log(`  completed: ${contentStats.completed}`);
+  console.log(`  failed: ${contentStats.failed}\n`);
+
+  // 显示前2个小节的详细信息
+  if (level3Chapters.length > 0) {
+    console.log(`🔍 示例小节 (前2个):`);
+    level3Chapters.slice(0, 2).forEach((c, i) => {
+      console.log(`\n  ${i + 1}. ${c.title}`);
+      console.log(`     level: ${c.level}`);
+      console.log(`     status (大纲): ${c.status}`);
+      console.log(`     contentStatus (内容): ${c.contentStatus || 'null'}`);
+      console.log(`     content: ${c.content ? `有 (${c.wordCount}字)` : '无'}`);
+    });
+  }
+
+  // 验证断点续传逻辑
+  console.log(`\n✅ 断点续传验证:`);
+  const leafNodes = level3Chapters;
+  const completedLeafNodes = leafNodes.filter(c => c.contentStatus === 'completed');
+  const pendingLeafNodes = leafNodes.filter(c => c.contentStatus !== 'completed');
+  
+  console.log(`  叶节点总数: ${leafNodes.length}`);
+  console.log(`  已完成内容: ${completedLeafNodes.length} (应跳过)`);
+  console.log(`  待生成内容: ${pendingLeafNodes.length} (应生成)`);
+  
+  if (completedLeafNodes.length > 0 && pendingLeafNodes.length > 0) {
+    console.log(`  ✅ 断点续传逻辑正确:能区分已完成和待生成`);
+  } else if (completedLeafNodes.length === 0 && pendingLeafNodes.length > 0) {
+    console.log(`  ℹ️  内容还未开始生成`);
+  }
+
+  await prisma.$disconnect();
+}
+
+testStatus().catch(console.error);