|
|
@@ -914,6 +914,345 @@ router.post('/books/:id/retry-chapter', async (ctx: Context) => {
|
|
|
}
|
|
|
});
|
|
|
|
|
|
+/**
|
|
|
+ * GET /api/book-generator/langgraph/books/:id/full-content
|
|
|
+ * 获取完整书籍内容(所有章节内容合并)
|
|
|
+ */
|
|
|
+router.get('/books/:id/full-content', async (ctx: Context) => {
|
|
|
+ try {
|
|
|
+ const bookId = ctx.params.id as string;
|
|
|
+ const book = await bookStore.getById(bookId);
|
|
|
+
|
|
|
+ if (!book) {
|
|
|
+ ctx.status = 404;
|
|
|
+ ctx.body = { code: 1, message: '书籍不存在' };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取章节树
|
|
|
+ const chapters = await bookStore.getChapterTree(bookId);
|
|
|
+
|
|
|
+ // 按层级和顺序构建内容
|
|
|
+ let fullContent = `# ${book.title}\n\n`;
|
|
|
+ if (book.description) {
|
|
|
+ fullContent += `## 简介\n${book.description}\n\n`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 递归添加章节内容
|
|
|
+ const addChapterContent = (chapter: any, indent: string = '') => {
|
|
|
+ if (chapter.content && chapter.contentStatus === 'completed') {
|
|
|
+ fullContent += `${indent}${chapter.number}. ${chapter.title}\n\n`;
|
|
|
+ fullContent += chapter.content + '\n\n';
|
|
|
+ }
|
|
|
+ // 处理子章节
|
|
|
+ const children = chapters.filter((c: any) => c.parentId === chapter.id);
|
|
|
+ children.forEach(child => {
|
|
|
+ addChapterContent(child, indent + ' ');
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // 添加一级章节
|
|
|
+ const level1Chapters = chapters.filter((c: any) => c.level === 1);
|
|
|
+ level1Chapters.forEach(chapter => addChapterContent(chapter));
|
|
|
+
|
|
|
+ ctx.body = {
|
|
|
+ code: 0,
|
|
|
+ message: 'success',
|
|
|
+ data: {
|
|
|
+ bookId,
|
|
|
+ title: book.title,
|
|
|
+ totalChapters: level1Chapters.length,
|
|
|
+ content: fullContent,
|
|
|
+ wordCount: book.estimatedWords,
|
|
|
+ },
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取完整内容失败:', error);
|
|
|
+ ctx.status = 500;
|
|
|
+ ctx.body = { code: 1, message: error instanceof Error ? error.message : '获取失败' };
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+/**
|
|
|
+ * POST /api/book-generator/langgraph/books/:id/outline
|
|
|
+ * 生成书籍大纲
|
|
|
+ */
|
|
|
+router.post('/books/:id/outline', async (ctx: Context) => {
|
|
|
+ try {
|
|
|
+ const bookId = ctx.params.id as string;
|
|
|
+ const book = await bookStore.getById(bookId);
|
|
|
+
|
|
|
+ if (!book) {
|
|
|
+ ctx.status = 404;
|
|
|
+ ctx.body = { code: 1, message: '书籍不存在' };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否已经有大纲
|
|
|
+ if (book.outline && book.outline.chapters && book.outline.chapters.length > 0) {
|
|
|
+ ctx.body = {
|
|
|
+ code: 0,
|
|
|
+ message: '大纲已存在,无需重复生成',
|
|
|
+ data: { outline: book.outline },
|
|
|
+ };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取书籍类型配置
|
|
|
+ const scaleConfig = getScaleConfig(book.bookScale || '标准教程');
|
|
|
+
|
|
|
+ // 调用AI生成大纲
|
|
|
+ const { generateOutline } = await import('./index');
|
|
|
+ const outline = await generateOutline(bookId, book.description, book.bookScale || '标准教程');
|
|
|
+
|
|
|
+ // 更新书籍的大纲
|
|
|
+ await bookStore.update(bookId, { outline });
|
|
|
+
|
|
|
+ ctx.body = {
|
|
|
+ code: 0,
|
|
|
+ message: '大纲生成成功',
|
|
|
+ data: { outline },
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ console.error('生成大纲失败:', error);
|
|
|
+ ctx.status = 500;
|
|
|
+ ctx.body = { code: 1, message: error instanceof Error ? error.message : '生成大纲失败' };
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+/**
|
|
|
+ * POST /api/book-generator/langgraph/books/:id/chapters
|
|
|
+ * 生成单个章节(或全部章节,取决于参数)
|
|
|
+ * body: { chapterNumber?: number } - 如果不传chapterNumber,则生成全部
|
|
|
+ */
|
|
|
+router.post('/books/:id/chapters', async (ctx: Context) => {
|
|
|
+ try {
|
|
|
+ const bookId = ctx.params.id as string;
|
|
|
+ const body = ctx.request.body as { chapterNumber?: number };
|
|
|
+ const book = await bookStore.getById(bookId);
|
|
|
+
|
|
|
+ if (!book) {
|
|
|
+ ctx.status = 404;
|
|
|
+ ctx.body = { code: 1, message: '书籍不存在' };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果指定了章节号,只生成单个章节
|
|
|
+ if (body.chapterNumber) {
|
|
|
+ // 检查章节是否存在
|
|
|
+ const chapters = await bookStore.getChapterTree(bookId);
|
|
|
+ const targetChapter = chapters.find((c: any) => c.number === body.chapterNumber);
|
|
|
+
|
|
|
+ if (!targetChapter) {
|
|
|
+ ctx.status = 404;
|
|
|
+ ctx.body = { code: 1, message: `章节 ${body.chapterNumber} 不存在` };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 异步生成章节内容
|
|
|
+ bookStore.generateSingleChapterContent(bookId, targetChapter.id).catch(err => {
|
|
|
+ console.error(`[Generate Chapter] 章节${body.chapterNumber}生成失败:`, err);
|
|
|
+ });
|
|
|
+
|
|
|
+ ctx.body = {
|
|
|
+ code: 0,
|
|
|
+ message: `章节 ${body.chapterNumber} 生成任务已启动`,
|
|
|
+ data: { chapterId: targetChapter.id, chapterNumber: body.chapterNumber },
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ // 没有指定章节号,触发整本书生成
|
|
|
+ ctx.body = {
|
|
|
+ code: 0,
|
|
|
+ message: '请使用 /generate 接口生成整本书',
|
|
|
+ data: { hint: '使用 POST /books/:id/generate' },
|
|
|
+ };
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('生成章节失败:', error);
|
|
|
+ ctx.status = 500;
|
|
|
+ ctx.body = { code: 1, message: error instanceof Error ? error.message : '生成章节失败' };
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+/**
|
|
|
+ * POST /api/book-generator/langgraph/books/:id/foreword
|
|
|
+ * 生成前言
|
|
|
+ */
|
|
|
+router.post('/books/:id/foreword', async (ctx: Context) => {
|
|
|
+ try {
|
|
|
+ const bookId = ctx.params.id as string;
|
|
|
+ const book = await bookStore.getById(bookId);
|
|
|
+
|
|
|
+ if (!book) {
|
|
|
+ ctx.status = 404;
|
|
|
+ ctx.body = { code: 1, message: '书籍不存在' };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调用LLM生成前言
|
|
|
+ const { callLLMWithMessages, ChatMessage } = await import('../../services/llm');
|
|
|
+
|
|
|
+ const messages: ChatMessage[] = [
|
|
|
+ {
|
|
|
+ role: 'system',
|
|
|
+ content: `你是一位专业的图书作者。请根据以下信息为书籍生成前言(前言通常介绍写作背景、目标读者、内容概要)。
|
|
|
+要求:
|
|
|
+1. 字数控制在300-500字
|
|
|
+2. 语言正式但亲切
|
|
|
+3. 不要使用 markdown 格式,直接输出纯文本
|
|
|
+
|
|
|
+输出格式:
|
|
|
+直接输出前言内容,不要包含任何标记。`,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ role: 'user',
|
|
|
+ content: `书籍信息:
|
|
|
+- 书名:${book.title}
|
|
|
+- 描述:${book.description}
|
|
|
+- 目标读者:${book.targetAudience || '普通读者'}
|
|
|
+- 风格:${book.style || '专业'}
|
|
|
+
|
|
|
+请生成这本书的前言。`,
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ const foreword = await callLLMWithMessages(messages);
|
|
|
+
|
|
|
+ // 更新书籍元数据
|
|
|
+ const metadata = book.metadata || {};
|
|
|
+ (metadata as any).foreword = foreword;
|
|
|
+ await bookStore.update(bookId, { metadata });
|
|
|
+
|
|
|
+ ctx.body = {
|
|
|
+ code: 0,
|
|
|
+ message: '前言生成成功',
|
|
|
+ data: { foreword },
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ console.error('生成前言失败:', error);
|
|
|
+ ctx.status = 500;
|
|
|
+ ctx.body = { code: 1, message: error instanceof Error ? error.message : '生成前言失败' };
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+/**
|
|
|
+ * POST /api/book-generator/langgraph/books/:id/afterword
|
|
|
+ * 生成后记
|
|
|
+ */
|
|
|
+router.post('/books/:id/afterword', async (ctx: Context) => {
|
|
|
+ try {
|
|
|
+ const bookId = ctx.params.id as string;
|
|
|
+ const book = await bookStore.getById(bookId);
|
|
|
+
|
|
|
+ if (!book) {
|
|
|
+ ctx.status = 404;
|
|
|
+ ctx.body = { code: 1, message: '书籍不存在' };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调用LLM生成后记
|
|
|
+ const { callLLMWithMessages, ChatMessage } = await import('../../services/llm');
|
|
|
+
|
|
|
+ const messages: ChatMessage[] = [
|
|
|
+ {
|
|
|
+ role: 'system',
|
|
|
+ content: `你是一位专业的图书作者。请为书籍生成后记(后记通常总结全书核心观点、分享写作心得、感谢读者)。
|
|
|
+要求:
|
|
|
+1. 字数控制在300-500字
|
|
|
+2. 语言真挚、诚恳
|
|
|
+3. 不要使用 markdown 格式,直接输出纯文本
|
|
|
+
|
|
|
+输出格式:
|
|
|
+直接输出后记内容,不要包含任何标记。`,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ role: 'user',
|
|
|
+ content: `书籍信息:
|
|
|
+- 书名:${book.title}
|
|
|
+- 描述:${book.description}
|
|
|
+- 章节数:${book.totalChapters}
|
|
|
+- 总字数:约${book.estimatedWords}字
|
|
|
+
|
|
|
+请生成这本书的后记。`,
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ const afterword = await callLLMWithMessages(messages);
|
|
|
+
|
|
|
+ // 更新书籍元数据
|
|
|
+ const metadata = book.metadata || {};
|
|
|
+ (metadata as any).afterword = afterword;
|
|
|
+ await bookStore.update(bookId, { metadata });
|
|
|
+
|
|
|
+ ctx.body = {
|
|
|
+ code: 0,
|
|
|
+ message: '后记生成成功',
|
|
|
+ data: { afterword },
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ console.error('生成后记失败:', error);
|
|
|
+ ctx.status = 500;
|
|
|
+ ctx.body = { code: 1, message: error instanceof Error ? error.message : '生成后记失败' };
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+/**
|
|
|
+ * GET /api/book-generator/langgraph/workflow/:bookId
|
|
|
+ * 获取书籍生成工作流状态
|
|
|
+ */
|
|
|
+router.get('/workflow/:bookId', async (ctx: Context) => {
|
|
|
+ try {
|
|
|
+ const bookId = ctx.params.bookId as string;
|
|
|
+ const book = await bookStore.getById(bookId);
|
|
|
+
|
|
|
+ if (!book) {
|
|
|
+ ctx.status = 404;
|
|
|
+ ctx.body = { code: 1, message: '书籍不存在' };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取章节树
|
|
|
+ const chapters = await bookStore.getChapterTree(bookId);
|
|
|
+
|
|
|
+ // 分析工作流状态
|
|
|
+ const completedChapters = chapters.filter((c: any) => c.status === 'completed').length;
|
|
|
+ const failedChapters = chapters.filter((c: any) => c.status === 'failed').length;
|
|
|
+ const pendingChapters = chapters.filter((c: any) => c.status === 'pending').length;
|
|
|
+ const generatingChapters = chapters.filter((c: any) => c.status === 'generating').length;
|
|
|
+
|
|
|
+ // 确定当前阶段
|
|
|
+ let phase: 'planning' | 'writing' | 'supplement' | 'done' = 'planning';
|
|
|
+ if (book.status === 'generating') {
|
|
|
+ phase = 'writing';
|
|
|
+ } else if (completedChapters > 0 && failedChapters === 0) {
|
|
|
+ phase = 'done';
|
|
|
+ } else if (failedChapters > 0) {
|
|
|
+ phase = 'supplement';
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx.body = {
|
|
|
+ code: 0,
|
|
|
+ message: 'success',
|
|
|
+ data: {
|
|
|
+ bookId,
|
|
|
+ status: book.status,
|
|
|
+ phase,
|
|
|
+ progress: book.progress || 0,
|
|
|
+ completedChapters,
|
|
|
+ failedChapters,
|
|
|
+ pendingChapters,
|
|
|
+ generatingChapters,
|
|
|
+ totalChapters: chapters.length,
|
|
|
+ failedChapterIds: chapters.filter((c: any) => c.status === 'failed').map((c: any) => c.id),
|
|
|
+ },
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取工作流状态失败:', error);
|
|
|
+ ctx.status = 500;
|
|
|
+ ctx.body = { code: 1, message: error instanceof Error ? error.message : '获取失败' };
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
/**
|
|
|
* GET /api/book-generator/langgraph/books/:id/chapters/:chapterId/content
|
|
|
* 获取指定章节的内容
|