|
|
@@ -10,7 +10,7 @@ import { bookStore, cancelAudioGeneration } from './book-generator.store';
|
|
|
import { assertBookAccess, assertChapterAccess } from './access-control';
|
|
|
import { prisma } from '../../models';
|
|
|
import { estimateBookWords, estimateAudioMinutesFromWords, checkBookGenerationQuota, checkAudioQuota, atomicReserveQuota, releaseQuota, AUDIO_BILLING_CONFIG } from '../subscription/subscription.service';
|
|
|
-import { optionalAuth } from '../../middleware/auth';
|
|
|
+import { optionalAuth, requireAuth } from '../../middleware/auth';
|
|
|
import { getAllBookTypes, getDetectableTypes, getBookTypeConfig, BOOK_TYPE_CONFIG, DETECTABLE_TYPES } from './book-type-config';
|
|
|
import { callLLMWithMessages, callLLM, ChatMessage } from '../../services/llm';
|
|
|
import { cleanLlmShortText, extractJsonFromResponse } from '../../services/llm/response-cleaner';
|
|
|
@@ -769,7 +769,7 @@ router.get('/public-books', async (ctx: Context) => {
|
|
|
* 获取当前用户自己的书籍列表(管理页专用)
|
|
|
* 只返回当前用户创建的书籍
|
|
|
*/
|
|
|
-router.get('/my-books', optionalAuth, async (ctx: Context) => {
|
|
|
+router.get('/my-books', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const userId = ctx.state.user?.userId;
|
|
|
if (!userId) {
|
|
|
@@ -791,7 +791,7 @@ router.get('/my-books', optionalAuth, async (ctx: Context) => {
|
|
|
* PUT /api/book-generator/langgraph/books/:id/publish
|
|
|
* 切换书籍公开状态
|
|
|
*/
|
|
|
-router.put('/books/:id/publish', optionalAuth, async (ctx: Context) => {
|
|
|
+router.put('/books/:id/publish', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -858,7 +858,7 @@ router.get('/books/:id', optionalAuth, async (ctx: Context) => {
|
|
|
* GET /api/book-generator/langgraph/books/:id/progress
|
|
|
* 获取书籍生成进度
|
|
|
*/
|
|
|
-router.get('/books/:id/progress', optionalAuth, async (ctx: Context) => {
|
|
|
+router.get('/books/:id/progress', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -917,7 +917,7 @@ router.get('/books/:id/progress', optionalAuth, async (ctx: Context) => {
|
|
|
* DELETE /api/book-generator/langgraph/books/:id
|
|
|
* 删除书籍
|
|
|
*/
|
|
|
-router.delete('/books/:id', optionalAuth, async (ctx: Context) => {
|
|
|
+router.delete('/books/:id', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -940,7 +940,7 @@ router.delete('/books/:id', optionalAuth, async (ctx: Context) => {
|
|
|
* POST /api/book-generator/langgraph/books/:id/generate
|
|
|
* 对已有书籍使用 LangGraph 生成
|
|
|
*/
|
|
|
-router.post('/books/:id/generate', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/generate', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -1097,7 +1097,7 @@ router.post('/books/:id/generate', optionalAuth, async (ctx: Context) => {
|
|
|
* POST /api/book-generator/langgraph/books/:id/audio
|
|
|
* 批量生成书籍所有小节的音频
|
|
|
*/
|
|
|
-router.post('/books/:id/audio', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/audio', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -1180,7 +1180,7 @@ router.post('/books/:id/audio', optionalAuth, async (ctx: Context) => {
|
|
|
* POST /api/book-generator/langgraph/books/:id/audio/cancel
|
|
|
* 取消书籍所有章节的音频生成
|
|
|
*/
|
|
|
-router.post('/books/:id/audio/cancel', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/audio/cancel', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -1226,7 +1226,7 @@ router.post('/books/:id/audio/cancel', optionalAuth, async (ctx: Context) => {
|
|
|
* GET /api/book-generator/langgraph/books/:id/video-status
|
|
|
* 获取书籍视频生成状态(用于前端轮询)
|
|
|
*/
|
|
|
-router.get('/books/:id/video-status', optionalAuth, async (ctx: Context) => {
|
|
|
+router.get('/books/:id/video-status', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -1282,7 +1282,7 @@ router.get('/books/:id/video-status', optionalAuth, async (ctx: Context) => {
|
|
|
* GET /api/book-generator/langgraph/books/:id/audio-status
|
|
|
* 获取书籍音频生成状态(用于前端轮询)
|
|
|
*/
|
|
|
-router.get('/books/:id/audio-status', optionalAuth, async (ctx: Context) => {
|
|
|
+router.get('/books/:id/audio-status', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -1338,7 +1338,7 @@ router.get('/books/:id/audio-status', optionalAuth, async (ctx: Context) => {
|
|
|
* GET /api/book-generator/langgraph/books/:id/failed-chapters
|
|
|
* 获取生成失败的小节列表
|
|
|
*/
|
|
|
-router.get('/books/:id/failed-chapters', optionalAuth, async (ctx: Context) => {
|
|
|
+router.get('/books/:id/failed-chapters', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -1392,7 +1392,7 @@ router.get('/books/:id/failed-chapters', optionalAuth, async (ctx: Context) => {
|
|
|
* 逐个调用 generateSingleChapterContent(只补单章,不碰大纲),
|
|
|
* 音频随后由 AudioScanner 兜底入队。
|
|
|
*/
|
|
|
-router.post('/books/:id/resume', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/resume', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -1492,7 +1492,7 @@ router.post('/books/:id/resume', optionalAuth, async (ctx: Context) => {
|
|
|
* POST /api/book-generator/langgraph/books/:id/retry-chapter
|
|
|
* 单独重试某个失败的小节
|
|
|
*/
|
|
|
-router.post('/books/:id/retry-chapter', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/retry-chapter', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
|
const { chapterId } = ctx.request.body as { chapterId: number };
|
|
|
@@ -1542,7 +1542,7 @@ router.post('/books/:id/retry-chapter', optionalAuth, async (ctx: Context) => {
|
|
|
* GET /api/book-generator/langgraph/books/:id/full-content
|
|
|
* 获取完整书籍内容(所有章节内容合并)
|
|
|
*/
|
|
|
-router.get('/books/:id/full-content', optionalAuth, async (ctx: Context) => {
|
|
|
+router.get('/books/:id/full-content', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -1608,7 +1608,7 @@ router.get('/books/:id/full-content', optionalAuth, async (ctx: Context) => {
|
|
|
* POST /api/book-generator/langgraph/books/:id/outline
|
|
|
* 生成书籍大纲
|
|
|
*/
|
|
|
-router.post('/books/:id/outline', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/outline', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -1663,7 +1663,7 @@ router.post('/books/:id/outline', optionalAuth, async (ctx: Context) => {
|
|
|
* 生成单个章节(或全部章节,取决于参数)
|
|
|
* body: { chapterNumber?: number } - 如果不传chapterNumber,则生成全部
|
|
|
*/
|
|
|
-router.post('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/chapters', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -1723,7 +1723,7 @@ router.post('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
|
|
|
* POST /api/book-generator/langgraph/books/:id/foreword
|
|
|
* 生成前言
|
|
|
*/
|
|
|
-router.post('/books/:id/foreword', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/foreword', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -1787,7 +1787,7 @@ router.post('/books/:id/foreword', optionalAuth, async (ctx: Context) => {
|
|
|
* POST /api/book-generator/langgraph/books/:id/afterword
|
|
|
* 生成后记
|
|
|
*/
|
|
|
-router.post('/books/:id/afterword', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/afterword', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -1851,7 +1851,7 @@ router.post('/books/:id/afterword', optionalAuth, async (ctx: Context) => {
|
|
|
* GET /api/book-generator/langgraph/workflow/:bookId
|
|
|
* 获取书籍生成工作流状态
|
|
|
*/
|
|
|
-router.get('/workflow/:bookId', optionalAuth, async (ctx: Context) => {
|
|
|
+router.get('/workflow/:bookId', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.bookId as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -1982,7 +1982,7 @@ router.get('/books/:id/chapters/:chapterId/content', optionalAuth, async (ctx: C
|
|
|
* POST /api/book-generator/langgraph/books/:id/chapters/:chapterId/content
|
|
|
* 保存章节内容(手工编辑后保存)
|
|
|
*/
|
|
|
-router.post('/books/:id/chapters/:chapterId/content', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/chapters/:chapterId/content', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const { id: bookId, chapterId } = ctx.params as { id: string; chapterId: string };
|
|
|
const chapterIdNum = parseInt(chapterId);
|
|
|
@@ -2043,7 +2043,7 @@ router.post('/books/:id/chapters/:chapterId/content', optionalAuth, async (ctx:
|
|
|
* POST /api/book-generator/langgraph/books/:id/chapters/batch-regenerate
|
|
|
* 批量重新生成章节内容
|
|
|
*/
|
|
|
-router.post('/books/:id/chapters/batch-regenerate', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/chapters/batch-regenerate', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const { id: bookId } = ctx.params as { id: string };
|
|
|
const { chapterIds } = ctx.request.body as { chapterIds: number[] };
|
|
|
@@ -2100,7 +2100,7 @@ router.post('/books/:id/chapters/batch-regenerate', optionalAuth, async (ctx: Co
|
|
|
* POST /api/book-generator/langgraph/books/:id/chapters/batch-delete
|
|
|
* 批量删除章节
|
|
|
*/
|
|
|
-router.post('/books/:id/chapters/batch-delete', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/chapters/batch-delete', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const { id: bookId } = ctx.params as { id: string };
|
|
|
const { chapterIds } = ctx.request.body as { chapterIds: number[] };
|
|
|
@@ -2161,7 +2161,7 @@ router.post('/books/:id/chapters/batch-delete', optionalAuth, async (ctx: Contex
|
|
|
* POST /api/book-generator/langgraph/books/:id/chapters/:chapterId/regenerate
|
|
|
* 重新生成指定章节的内容(仅叶节点允许)
|
|
|
*/
|
|
|
-router.post('/books/:id/chapters/:chapterId/regenerate', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/chapters/:chapterId/regenerate', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const { id: bookId, chapterId } = ctx.params as { id: string; chapterId: string };
|
|
|
const chapterIdNum = parseInt(chapterId);
|
|
|
@@ -2229,7 +2229,7 @@ router.post('/books/:id/chapters/:chapterId/regenerate', optionalAuth, async (ct
|
|
|
* POST /api/book-generator/langgraph/books/:id/chapters/:chapterId/regenerate-audio
|
|
|
* 重新生成章节音频(仅叶节点)
|
|
|
*/
|
|
|
-router.post('/books/:id/chapters/:chapterId/regenerate-audio', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/chapters/:chapterId/regenerate-audio', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const { id: bookId, chapterId } = ctx.params as { id: string; chapterId: string };
|
|
|
const chapterIdNum = parseInt(chapterId);
|
|
|
@@ -2329,7 +2329,7 @@ router.post('/books/:id/chapters/:chapterId/regenerate-audio', optionalAuth, asy
|
|
|
* POST /api/book-generator/langgraph/books/:id/chapters/:chapterId/video
|
|
|
* 生成单个章节视频
|
|
|
*/
|
|
|
-router.post('/books/:id/chapters/:chapterId/video', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/chapters/:chapterId/video', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const { id: bookId, chapterId } = ctx.params as { id: string; chapterId: string };
|
|
|
const chapterIdNum = parseInt(chapterId);
|
|
|
@@ -2406,7 +2406,7 @@ router.post('/books/:id/chapters/:chapterId/video', optionalAuth, async (ctx: Co
|
|
|
* POST /api/book-generator/langgraph/books/:id/videos
|
|
|
* 批量生成书籍所有章节视频
|
|
|
*/
|
|
|
-router.post('/books/:id/videos', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/videos', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -2531,7 +2531,7 @@ router.post('/books/:id/videos', optionalAuth, async (ctx: Context) => {
|
|
|
* POST /api/book-generator/langgraph/books/:id/merge-audio
|
|
|
* 合并叶节点音频到上级章节
|
|
|
*/
|
|
|
-router.post('/books/:id/merge-audio', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/merge-audio', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -2663,7 +2663,7 @@ router.post('/books/:id/merge-audio', optionalAuth, async (ctx: Context) => {
|
|
|
* POST /api/book-generator/langgraph/books/:id/merge-video
|
|
|
* 合并叶节点视频到上级章节
|
|
|
*/
|
|
|
-router.post('/books/:id/merge-video', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/merge-video', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -2880,7 +2880,7 @@ async function mergeChapterVideos(parentChapter: any, childChapters: any[], user
|
|
|
* DEBUG: 手动触发章节音频生成(强制从content_completed重新开始)
|
|
|
* POST /api/book-generator/debug/trigger-audio
|
|
|
*/
|
|
|
-router.post('/debug/trigger-audio', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/debug/trigger-audio', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const { chapterId } = ctx.request.body as { chapterId?: number };
|
|
|
if (!chapterId) {
|
|
|
@@ -2927,7 +2927,7 @@ router.post('/debug/trigger-audio', optionalAuth, async (ctx: Context) => {
|
|
|
* POST /api/book-generator/langgraph/books/:id/interactive/plan
|
|
|
* 异步启动深度规划,前端通过 GET 同名接口轮询结果
|
|
|
*/
|
|
|
-router.post('/books/:id/interactive/plan', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/interactive/plan', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -3004,7 +3004,7 @@ router.post('/books/:id/interactive/plan', optionalAuth, async (ctx: Context) =>
|
|
|
* GET /api/book-generator/langgraph/books/:id/interactive/plan
|
|
|
* 轮询获取深度规划结果
|
|
|
*/
|
|
|
-router.get('/books/:id/interactive/plan', optionalAuth, async (ctx: Context) => {
|
|
|
+router.get('/books/:id/interactive/plan', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -3061,7 +3061,7 @@ router.get('/books/:id/interactive/plan', optionalAuth, async (ctx: Context) =>
|
|
|
* PUT /api/book-generator/langgraph/books/:id/interactive/plan
|
|
|
* 保存用户修改后的 plan
|
|
|
*/
|
|
|
-router.put('/books/:id/interactive/plan', optionalAuth, async (ctx: Context) => {
|
|
|
+router.put('/books/:id/interactive/plan', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id;
|
|
|
const { plan } = ctx.request.body as { plan: string };
|
|
|
@@ -3096,7 +3096,7 @@ router.put('/books/:id/interactive/plan', optionalAuth, async (ctx: Context) =>
|
|
|
* POST /api/book-generator/langgraph/books/:id/interactive/outline
|
|
|
* 异步启动大纲生成,前端通过 GET 同名接口轮询结果
|
|
|
*/
|
|
|
-router.post('/books/:id/interactive/outline', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/interactive/outline', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -3171,7 +3171,7 @@ router.post('/books/:id/interactive/outline', optionalAuth, async (ctx: Context)
|
|
|
* GET /api/book-generator/langgraph/books/:id/interactive/outline
|
|
|
* 轮询获取大纲生成结果
|
|
|
*/
|
|
|
-router.get('/books/:id/interactive/outline', optionalAuth, async (ctx: Context) => {
|
|
|
+router.get('/books/:id/interactive/outline', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
@@ -3226,7 +3226,7 @@ router.get('/books/:id/interactive/outline', optionalAuth, async (ctx: Context)
|
|
|
* PUT /api/book-generator/langgraph/books/:id/interactive/outline
|
|
|
* 保存用户修改后的大纲,并重建章节树
|
|
|
*/
|
|
|
-router.put('/books/:id/interactive/outline', optionalAuth, async (ctx: Context) => {
|
|
|
+router.put('/books/:id/interactive/outline', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id;
|
|
|
const { outline } = ctx.request.body as { outline: any };
|
|
|
@@ -3309,7 +3309,7 @@ router.put('/books/:id/interactive/outline', optionalAuth, async (ctx: Context)
|
|
|
* POST /api/book-generator/langgraph/books/:id/interactive/generate
|
|
|
* 运行并行内容生成 + 连贯性编辑
|
|
|
*/
|
|
|
-router.post('/books/:id/interactive/generate', optionalAuth, async (ctx: Context) => {
|
|
|
+router.post('/books/:id/interactive/generate', requireAuth, async (ctx: Context) => {
|
|
|
try {
|
|
|
const bookId = ctx.params.id;
|
|
|
const accessUserId = ctx.state.user?.userId || TEST_USER_ID;
|