|
@@ -77,6 +77,18 @@ const GraphState = Annotation.Root({
|
|
|
}),
|
|
}),
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+// ============ 进度常量 ============
|
|
|
|
|
+
|
|
|
|
|
+const PROGRESS = {
|
|
|
|
|
+ OUTLINE_DONE: 5,
|
|
|
|
|
+ SECTIONS_DONE: 10,
|
|
|
|
|
+ SUBSECTIONS_DONE: 15,
|
|
|
|
|
+ CONTENT_START: 15,
|
|
|
|
|
+ CONTENT_END: 95,
|
|
|
|
|
+ FOREWORD_DONE: 95,
|
|
|
|
|
+ AFTERWORD_DONE: 100,
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
// ============ 规模配置 ============
|
|
// ============ 规模配置 ============
|
|
|
|
|
|
|
|
const SCALE_CHAPTER_RANGE = {
|
|
const SCALE_CHAPTER_RANGE = {
|
|
@@ -243,6 +255,27 @@ const AFTERWORD_SYSTEM_PROMPT = `你是一位资深作家,擅长撰写令人
|
|
|
- 不要写"感谢读者"之类的套话
|
|
- 不要写"感谢读者"之类的套话
|
|
|
- 结尾要有力量感,可以是金句、问题或开放性思考`;
|
|
- 结尾要有力量感,可以是金句、问题或开放性思考`;
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * 小节内容写作提示词
|
|
|
|
|
+ */
|
|
|
|
|
+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 格式标记`;
|
|
|
|
|
+
|
|
|
// ============ 动态提示词配置表 ============
|
|
// ============ 动态提示词配置表 ============
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -893,13 +926,13 @@ async function generateOutlineNode(state: typeof GraphState.State): Promise<Part
|
|
|
if (!outline) throw new Error('大纲解析失败');
|
|
if (!outline) throw new Error('大纲解析失败');
|
|
|
|
|
|
|
|
const totalChapters = outline.chapters.length;
|
|
const totalChapters = outline.chapters.length;
|
|
|
- await bookStore.update(state.bookId, { totalChapters, outlineJson: JSON.stringify(outline), status: 'planning', progress: 5 });
|
|
|
|
|
|
|
+ await bookStore.update(state.bookId, { totalChapters, outlineJson: JSON.stringify(outline), status: 'planning', progress: PROGRESS.OUTLINE_DONE });
|
|
|
await bookStore.createChapters(state.bookId, outline.chapters.map(c => ({
|
|
await bookStore.createChapters(state.bookId, outline.chapters.map(c => ({
|
|
|
number: c.number, title: c.title, summary: c.summary, keyPoints: c.keyPoints, estimatedWords: c.estimatedWords,
|
|
number: c.number, title: c.title, summary: c.summary, keyPoints: c.keyPoints, estimatedWords: c.estimatedWords,
|
|
|
})));
|
|
})));
|
|
|
|
|
|
|
|
console.log('[LangGraph] 大纲生成完成,章节数:', outline.chapters.length);
|
|
console.log('[LangGraph] 大纲生成完成,章节数:', outline.chapters.length);
|
|
|
- return { progress: 5 };
|
|
|
|
|
|
|
+ return { progress: PROGRESS.OUTLINE_DONE };
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('[LangGraph] 大纲生成失败:', error);
|
|
console.error('[LangGraph] 大纲生成失败:', error);
|
|
|
return { error: error instanceof Error ? error.message : '失败', finished: true };
|
|
return { error: error instanceof Error ? error.message : '失败', finished: true };
|
|
@@ -916,7 +949,7 @@ async function generateSectionsNode(state: typeof GraphState.State): Promise<Par
|
|
|
const book = await bookStore.getById(state.bookId);
|
|
const book = await bookStore.getById(state.bookId);
|
|
|
if (!book || !book.outline) {
|
|
if (!book || !book.outline) {
|
|
|
console.log('[LangGraph] 无大纲,跳过节生成');
|
|
console.log('[LangGraph] 无大纲,跳过节生成');
|
|
|
- return { finished: true, progress: 10 };
|
|
|
|
|
|
|
+ return { finished: true, progress: PROGRESS.SECTIONS_DONE };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
@@ -992,11 +1025,11 @@ async function generateSectionsNode(state: typeof GraphState.State): Promise<Par
|
|
|
const updatedOutline = { ...book.outline, chapters: updatedChapters };
|
|
const updatedOutline = { ...book.outline, chapters: updatedChapters };
|
|
|
await bookStore.update(state.bookId, {
|
|
await bookStore.update(state.bookId, {
|
|
|
outlineJson: JSON.stringify(updatedOutline),
|
|
outlineJson: JSON.stringify(updatedOutline),
|
|
|
- progress: 10,
|
|
|
|
|
|
|
+ progress: PROGRESS.SECTIONS_DONE,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
console.log('[LangGraph] 节大纲生成完成');
|
|
console.log('[LangGraph] 节大纲生成完成');
|
|
|
- return { progress: 10 };
|
|
|
|
|
|
|
+ return { progress: PROGRESS.SECTIONS_DONE };
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('[LangGraph] 节大纲生成失败:', error);
|
|
console.error('[LangGraph] 节大纲生成失败:', error);
|
|
|
return { error: error instanceof Error ? error.message : '失败', finished: true };
|
|
return { error: error instanceof Error ? error.message : '失败', finished: true };
|
|
@@ -1013,7 +1046,7 @@ async function generateSubsectionsNode(state: typeof GraphState.State): Promise<
|
|
|
const book = await bookStore.getById(state.bookId);
|
|
const book = await bookStore.getById(state.bookId);
|
|
|
if (!book || !book.outline) {
|
|
if (!book || !book.outline) {
|
|
|
console.log('[LangGraph] 无大纲,跳过小节生成');
|
|
console.log('[LangGraph] 无大纲,跳过小节生成');
|
|
|
- return { finished: true, progress: 15 };
|
|
|
|
|
|
|
+ return { finished: true, progress: PROGRESS.SUBSECTIONS_DONE };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
@@ -1114,11 +1147,11 @@ async function generateSubsectionsNode(state: typeof GraphState.State): Promise<
|
|
|
const updatedOutline = { ...book.outline, chapters: updatedChapters };
|
|
const updatedOutline = { ...book.outline, chapters: updatedChapters };
|
|
|
await bookStore.update(state.bookId, {
|
|
await bookStore.update(state.bookId, {
|
|
|
outlineJson: JSON.stringify(updatedOutline),
|
|
outlineJson: JSON.stringify(updatedOutline),
|
|
|
- progress: 15,
|
|
|
|
|
|
|
+ progress: PROGRESS.SUBSECTIONS_DONE,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
console.log('[LangGraph] 小节大纲生成完成');
|
|
console.log('[LangGraph] 小节大纲生成完成');
|
|
|
- return { progress: 15 };
|
|
|
|
|
|
|
+ return { progress: PROGRESS.SUBSECTIONS_DONE };
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('[LangGraph] 小节大纲生成失败:', error);
|
|
console.error('[LangGraph] 小节大纲生成失败:', error);
|
|
|
return { error: error instanceof Error ? error.message : '失败', finished: true };
|
|
return { error: error instanceof Error ? error.message : '失败', finished: true };
|
|
@@ -1131,13 +1164,13 @@ async function writeChaptersNode(state: typeof GraphState.State): Promise<Partia
|
|
|
const book = await bookStore.getById(state.bookId);
|
|
const book = await bookStore.getById(state.bookId);
|
|
|
if (!book || !book.outline) {
|
|
if (!book || !book.outline) {
|
|
|
console.log('[LangGraph] 无大纲或书籍,跳过章节生成');
|
|
console.log('[LangGraph] 无大纲或书籍,跳过章节生成');
|
|
|
- return { finished: true, progress: 90 };
|
|
|
|
|
|
|
+ return { finished: true, progress: PROGRESS.CONTENT_END };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const bookIdNum = parseInt(state.bookId);
|
|
const bookIdNum = parseInt(state.bookId);
|
|
|
|
|
|
|
|
// 从数据库获取所有小节(level=3)
|
|
// 从数据库获取所有小节(level=3)
|
|
|
- const subsections = await prisma.bookChapter.findMany({
|
|
|
|
|
|
|
+ const allSubsections = await prisma.bookChapter.findMany({
|
|
|
where: { bookId: bookIdNum, level: 3 },
|
|
where: { bookId: bookIdNum, level: 3 },
|
|
|
orderBy: [
|
|
orderBy: [
|
|
|
{ parentId: 'asc' }, // 先按父节点排序
|
|
{ parentId: 'asc' }, // 先按父节点排序
|
|
@@ -1154,20 +1187,32 @@ async function writeChaptersNode(state: typeof GraphState.State): Promise<Partia
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ // 筛选出未完成的小节(pending 或 failed),支持断点续传
|
|
|
|
|
+ const subsections = allSubsections.filter(s => s.status !== 'completed');
|
|
|
|
|
+ const completedCount = allSubsections.length - subsections.length;
|
|
|
|
|
+
|
|
|
|
|
+ if (completedCount > 0) {
|
|
|
|
|
+ console.log(`[LangGraph] ✅ 跳过 ${completedCount} 个已完成的小节,待生成 ${subsections.length} 个`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (subsections.length === 0) {
|
|
if (subsections.length === 0) {
|
|
|
- console.log('[LangGraph] 没有小节,跳过内容生成');
|
|
|
|
|
- return { finished: true, progress: 90 };
|
|
|
|
|
|
|
+ console.log('[LangGraph] ✅ 所有小节已完成,跳过内容生成');
|
|
|
|
|
+ return { finished: true, progress: 95 };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 构建父节点映射,便于获取上下文
|
|
// 构建父节点映射,便于获取上下文
|
|
|
- const allChapters = await prisma.bookChapter.findMany({
|
|
|
|
|
- where: { bookId: bookIdNum },
|
|
|
|
|
- orderBy: { number: 'asc' }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
const chapterMap = new Map<number, any>();
|
|
const chapterMap = new Map<number, any>();
|
|
|
const sectionMap = new Map<number, any>();
|
|
const sectionMap = new Map<number, any>();
|
|
|
- allChapters.forEach(c => {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 重新查询章节和节来构建映射
|
|
|
|
|
+ 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 === 1) chapterMap.set(c.id, c);
|
|
|
if (c.level === 2) sectionMap.set(c.id, c);
|
|
if (c.level === 2) sectionMap.set(c.id, c);
|
|
|
});
|
|
});
|
|
@@ -1183,7 +1228,7 @@ async function writeChaptersNode(state: typeof GraphState.State): Promise<Partia
|
|
|
console.log(`[LangGraph] 当前已生成字数: ${currentWordCount}`);
|
|
console.log(`[LangGraph] 当前已生成字数: ${currentWordCount}`);
|
|
|
console.log(`[LangGraph] 待生成小节数: ${subsections.length}`);
|
|
console.log(`[LangGraph] 待生成小节数: ${subsections.length}`);
|
|
|
|
|
|
|
|
- // 计算总小节数用于进度
|
|
|
|
|
|
|
+ // 计算总小节数用于进度(从已完成数量开始)
|
|
|
const totalSubsections = allSubsections.length;
|
|
const totalSubsections = allSubsections.length;
|
|
|
let completedSubsections = completedCount; // 从已完成的数量开始
|
|
let completedSubsections = completedCount; // 从已完成的数量开始
|
|
|
|
|
|
|
@@ -1237,7 +1282,7 @@ async function writeChaptersNode(state: typeof GraphState.State): Promise<Partia
|
|
|
await bookStore.updateChapterById(subsection.id, { content, wordCount, status: 'completed' });
|
|
await bookStore.updateChapterById(subsection.id, { content, wordCount, status: 'completed' });
|
|
|
|
|
|
|
|
// 更新进度
|
|
// 更新进度
|
|
|
- const progress = Math.round((completedSubsections / totalSubsections) * 80) + 15;
|
|
|
|
|
|
|
+ const progress = Math.round((completedSubsections / totalSubsections) * (PROGRESS.CONTENT_END - PROGRESS.CONTENT_START)) + PROGRESS.CONTENT_START;
|
|
|
await bookStore.update(state.bookId, { progress, status: 'generating' });
|
|
await bookStore.update(state.bookId, { progress, status: 'generating' });
|
|
|
console.log(`[LangGraph] 小节「${subsection.title}」完成 (${completedSubsections}/${totalSubsections}),累计${currentWordCount}字,进度${progress}%`);
|
|
console.log(`[LangGraph] 小节「${subsection.title}」完成 (${completedSubsections}/${totalSubsections}),累计${currentWordCount}字,进度${progress}%`);
|
|
|
|
|
|
|
@@ -1256,8 +1301,8 @@ async function writeChaptersNode(state: typeof GraphState.State): Promise<Partia
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
- currentChapter: allChapters.length,
|
|
|
|
|
- progress: 90,
|
|
|
|
|
|
|
+ currentChapter: chaptersAndSections.filter(c => c.level === 1).length,
|
|
|
|
|
+ progress: PROGRESS.CONTENT_END,
|
|
|
error: failedChapters.length > 0 ? `小节${failedChapters.join(',')}失败` : undefined
|
|
error: failedChapters.length > 0 ? `小节${failedChapters.join(',')}失败` : undefined
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
@@ -1285,35 +1330,18 @@ function buildSubsectionContentMessages(topic: string, chapterTitle: string, cha
|
|
|
];
|
|
];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 小节内容写作提示词
|
|
|
|
|
-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 格式标记`;
|
|
|
|
|
|
|
+// ============ LangGraph 节点 ============
|
|
|
|
|
|
|
|
async function writeForewordNode(state: typeof GraphState.State): Promise<Partial<typeof GraphState.State>> {
|
|
async function writeForewordNode(state: typeof GraphState.State): Promise<Partial<typeof GraphState.State>> {
|
|
|
console.log('[LangGraph] 生成前言, bookId:', state.bookId);
|
|
console.log('[LangGraph] 生成前言, bookId:', state.bookId);
|
|
|
try {
|
|
try {
|
|
|
const messages = buildForewordMessages(state.topic);
|
|
const messages = buildForewordMessages(state.topic);
|
|
|
const foreword = await callLLMWithMessages(messages);
|
|
const foreword = await callLLMWithMessages(messages);
|
|
|
- await bookStore.update(state.bookId, { foreword, progress: 95 });
|
|
|
|
|
- return { progress: 95 };
|
|
|
|
|
|
|
+ await bookStore.update(state.bookId, { foreword, progress: PROGRESS.FOREWORD_DONE });
|
|
|
|
|
+ return { progress: PROGRESS.FOREWORD_DONE };
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('[LangGraph] 前言生成失败:', error);
|
|
console.error('[LangGraph] 前言生成失败:', error);
|
|
|
- return { progress: 95 };
|
|
|
|
|
|
|
+ return { progress: PROGRESS.FOREWORD_DONE };
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1323,10 +1351,10 @@ async function writeAfterwordNode(state: typeof GraphState.State): Promise<Parti
|
|
|
const messages = buildAfterwordMessages(state.topic);
|
|
const messages = buildAfterwordMessages(state.topic);
|
|
|
const afterword = await callLLMWithMessages(messages);
|
|
const afterword = await callLLMWithMessages(messages);
|
|
|
await bookStore.update(state.bookId, { afterword });
|
|
await bookStore.update(state.bookId, { afterword });
|
|
|
- return { finished: true, progress: 100 };
|
|
|
|
|
|
|
+ return { finished: true, progress: PROGRESS.AFTERWORD_DONE };
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('[LangGraph] 后记生成失败:', error);
|
|
console.error('[LangGraph] 后记生成失败:', error);
|
|
|
- return { finished: true, progress: 100 };
|
|
|
|
|
|
|
+ return { finished: true, progress: PROGRESS.AFTERWORD_DONE };
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|