|
|
@@ -914,6 +914,225 @@ router.post('/books/:id/retry-chapter', async (ctx: Context) => {
|
|
|
}
|
|
|
});
|
|
|
|
|
|
+/**
|
|
|
+ * GET /api/book-generator/langgraph/books/:id/chapters/:chapterId/content
|
|
|
+ * 获取指定章节的内容
|
|
|
+ */
|
|
|
+router.get('/books/:id/chapters/:chapterId/content', optionalAuth, async (ctx: Context) => {
|
|
|
+ try {
|
|
|
+ const { id: bookId, chapterId } = ctx.params as { id: string; chapterId: string };
|
|
|
+ const chapterIdNum = parseInt(chapterId);
|
|
|
+
|
|
|
+ // 获取书籍
|
|
|
+ const book = await bookStore.getById(bookId);
|
|
|
+ if (!book) {
|
|
|
+ ctx.status = 404;
|
|
|
+ ctx.body = { code: 1, message: '书籍不存在' };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取章节
|
|
|
+ const chapter = await prisma.bookChapter.findUnique({
|
|
|
+ where: { id: chapterIdNum },
|
|
|
+ });
|
|
|
+ if (!chapter) {
|
|
|
+ ctx.status = 404;
|
|
|
+ ctx.body = { code: 1, message: '章节不存在' };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否属于该书籍
|
|
|
+ if (chapter.bookId !== parseInt(bookId)) {
|
|
|
+ ctx.status = 404;
|
|
|
+ ctx.body = { code: 1, message: '章节不属于该书籍' };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx.body = {
|
|
|
+ code: 0,
|
|
|
+ message: 'success',
|
|
|
+ data: {
|
|
|
+ id: chapter.id,
|
|
|
+ bookId: chapter.bookId,
|
|
|
+ number: chapter.number,
|
|
|
+ title: chapter.title,
|
|
|
+ content: chapter.content || '',
|
|
|
+ wordCount: chapter.wordCount || 0,
|
|
|
+ status: chapter.status,
|
|
|
+ contentStatus: (chapter as any).contentStatus || 'pending',
|
|
|
+ level: chapter.level,
|
|
|
+ parentId: chapter.parentId,
|
|
|
+ summary: chapter.summary || '',
|
|
|
+ keyPoints: chapter.keyPoints ? JSON.parse(chapter.keyPoints) : [],
|
|
|
+ audioUrl: chapter.audioUrl || '',
|
|
|
+ videoUrl: chapter.videoUrl || '',
|
|
|
+ isPublic: chapter.isPublic || false,
|
|
|
+ },
|
|
|
+ };
|
|
|
+ } 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/:chapterId/content
|
|
|
+ * 保存章节内容(手工编辑后保存)
|
|
|
+ */
|
|
|
+router.post('/books/:id/chapters/:chapterId/content', optionalAuth, async (ctx: Context) => {
|
|
|
+ try {
|
|
|
+ const { id: bookId, chapterId } = ctx.params as { id: string; chapterId: string };
|
|
|
+ const chapterIdNum = parseInt(chapterId);
|
|
|
+ const { content } = ctx.request.body as { content: string };
|
|
|
+
|
|
|
+ if (content === undefined) {
|
|
|
+ ctx.status = 400;
|
|
|
+ ctx.body = { code: 1, message: '内容不能为空' };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取章节
|
|
|
+ const chapter = await prisma.bookChapter.findUnique({
|
|
|
+ where: { id: chapterIdNum },
|
|
|
+ });
|
|
|
+ if (!chapter) {
|
|
|
+ ctx.status = 404;
|
|
|
+ ctx.body = { code: 1, message: '章节不存在' };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否属于该书籍
|
|
|
+ if (chapter.bookId !== parseInt(bookId)) {
|
|
|
+ ctx.status = 404;
|
|
|
+ ctx.body = { code: 1, message: '章节不属于该书籍' };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算字数
|
|
|
+ const wordCount = content.replace(/<[^>]*>/g, '').length;
|
|
|
+
|
|
|
+ // 更新章节内容
|
|
|
+ await bookStore.updateChapterById(chapterIdNum, {
|
|
|
+ content,
|
|
|
+ wordCount,
|
|
|
+ contentStatus: 'completed',
|
|
|
+ });
|
|
|
+
|
|
|
+ ctx.body = {
|
|
|
+ code: 0,
|
|
|
+ message: '保存成功',
|
|
|
+ data: { chapterId: chapterIdNum, wordCount },
|
|
|
+ };
|
|
|
+ } 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/batch-regenerate
|
|
|
+ * 批量重新生成章节内容
|
|
|
+ */
|
|
|
+router.post('/books/:id/chapters/batch-regenerate', optionalAuth, async (ctx: Context) => {
|
|
|
+ try {
|
|
|
+ const { id: bookId } = ctx.params as { id: string };
|
|
|
+ const { chapterIds } = ctx.request.body as { chapterIds: number[] };
|
|
|
+
|
|
|
+ if (!chapterIds || !Array.isArray(chapterIds) || chapterIds.length === 0) {
|
|
|
+ ctx.status = 400;
|
|
|
+ ctx.body = { code: 1, message: '请选择要重新生成的章节' };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取书籍
|
|
|
+ const book = await bookStore.getById(bookId);
|
|
|
+ if (!book) {
|
|
|
+ ctx.status = 404;
|
|
|
+ ctx.body = { code: 1, message: '书籍不存在' };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重置并触发每个选中章节的重新生成
|
|
|
+ for (const chapterId of chapterIds) {
|
|
|
+ await bookStore.updateChapterById(chapterId, {
|
|
|
+ status: 'pending',
|
|
|
+ contentStatus: 'pending',
|
|
|
+ content: null,
|
|
|
+ });
|
|
|
+ // 后台异步执行
|
|
|
+ bookStore.generateSingleChapterContent(bookId, chapterId).catch(err => {
|
|
|
+ console.error(`[BatchRegenerate] 章节${chapterId}内容生成失败:`, err);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx.body = {
|
|
|
+ code: 0,
|
|
|
+ message: `已提交 ${chapterIds.length} 个章节的重新生成任务`,
|
|
|
+ data: { count: chapterIds.length },
|
|
|
+ };
|
|
|
+ } 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/batch-delete
|
|
|
+ * 批量删除章节
|
|
|
+ */
|
|
|
+router.post('/books/:id/chapters/batch-delete', optionalAuth, async (ctx: Context) => {
|
|
|
+ try {
|
|
|
+ const { id: bookId } = ctx.params as { id: string };
|
|
|
+ const { chapterIds } = ctx.request.body as { chapterIds: number[] };
|
|
|
+
|
|
|
+ if (!chapterIds || !Array.isArray(chapterIds) || chapterIds.length === 0) {
|
|
|
+ ctx.status = 400;
|
|
|
+ ctx.body = { code: 1, message: '请选择要删除的章节' };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取书籍
|
|
|
+ const book = await bookStore.getById(bookId);
|
|
|
+ if (!book) {
|
|
|
+ ctx.status = 404;
|
|
|
+ ctx.body = { code: 1, message: '书籍不存在' };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 删除章节(同时会删除子章节,因为没有设置级联删除,需要手动处理)
|
|
|
+ // 对于每个要删除的章节,先删除其所有子节点,再删除自身
|
|
|
+ for (const chapterId of chapterIds) {
|
|
|
+ // 删除子章节(level > 当前章节)
|
|
|
+ await prisma.bookChapter.deleteMany({
|
|
|
+ where: {
|
|
|
+ bookId: parseInt(bookId),
|
|
|
+ parentId: chapterId,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ // 删除自身
|
|
|
+ await prisma.bookChapter.deleteMany({
|
|
|
+ where: {
|
|
|
+ id: chapterId,
|
|
|
+ bookId: parseInt(bookId),
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx.body = {
|
|
|
+ code: 0,
|
|
|
+ message: `已删除 ${chapterIds.length} 个章节`,
|
|
|
+ data: { count: chapterIds.length },
|
|
|
+ };
|
|
|
+ } 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/:chapterId/regenerate
|
|
|
* 重新生成指定章节的内容(仅叶节点允许)
|