|
@@ -437,9 +437,17 @@ router.post('/books', optionalAuth, async (ctx: Context) => {
|
|
|
generateForeword?: boolean;
|
|
generateForeword?: boolean;
|
|
|
generateAfterword?: boolean;
|
|
generateAfterword?: boolean;
|
|
|
immediateGenerate?: boolean;
|
|
immediateGenerate?: boolean;
|
|
|
|
|
+ autoGenerateContent?: boolean;
|
|
|
|
|
+ autoGenerateAudio?: boolean;
|
|
|
voiceSpeed?: number;
|
|
voiceSpeed?: number;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ // 自动生成开关:未传时默认 true(与历史行为一致)
|
|
|
|
|
+ // - autoGenerateContent=false:创建后停在 draft 状态,等用户手动触发
|
|
|
|
|
+ // - autoGenerateAudio=false:内容生成完成后 AudioScanner 跳过该书,等用户手动触发
|
|
|
|
|
+ const autoGenContent = body.autoGenerateContent ?? true;
|
|
|
|
|
+ const autoGenAudio = body.autoGenerateAudio ?? true;
|
|
|
|
|
+
|
|
|
if (!body.description) {
|
|
if (!body.description) {
|
|
|
ctx.status = 400;
|
|
ctx.status = 400;
|
|
|
ctx.body = { code: 1, message: '描述不能为空' };
|
|
ctx.body = { code: 1, message: '描述不能为空' };
|
|
@@ -500,6 +508,8 @@ router.post('/books', optionalAuth, async (ctx: Context) => {
|
|
|
totalChapters: scaleConfig.isShortArticle ? 1 : scaleConfig.chapters,
|
|
totalChapters: scaleConfig.isShortArticle ? 1 : scaleConfig.chapters,
|
|
|
estimatedWords: totalWords,
|
|
estimatedWords: totalWords,
|
|
|
voiceSpeed: finalVoiceSpeed,
|
|
voiceSpeed: finalVoiceSpeed,
|
|
|
|
|
+ autoGenerateContent: autoGenContent,
|
|
|
|
|
+ autoGenerateAudio: autoGenAudio,
|
|
|
});
|
|
});
|
|
|
console.log(`[LangGraph.fastPath] finalVoiceSpeed=${finalVoiceSpeed}, type=${typeof finalVoiceSpeed}, body.voiceSpeed=${body.voiceSpeed}`);
|
|
console.log(`[LangGraph.fastPath] finalVoiceSpeed=${finalVoiceSpeed}, type=${typeof finalVoiceSpeed}, body.voiceSpeed=${body.voiceSpeed}`);
|
|
|
|
|
|
|
@@ -508,6 +518,17 @@ router.post('/books', optionalAuth, async (ctx: Context) => {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 用户取消自动生成文本:仅创建草稿,等待用户手动触发
|
|
|
|
|
+ if (!autoGenContent) {
|
|
|
|
|
+ console.log(`[LangGraph] 用户关闭自动生成文本,创建后保持 draft: bookId=${book.id}`);
|
|
|
|
|
+ ctx.body = {
|
|
|
|
|
+ code: 0,
|
|
|
|
|
+ message: '书籍创建成功,未启用自动生成文本',
|
|
|
|
|
+ data: { book, genStage: 'draft', mode: 'manual_content' },
|
|
|
|
|
+ };
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
await bookStore.update(book.id, { genStage: 'outlining' });
|
|
await bookStore.update(book.id, { genStage: 'outlining' });
|
|
|
langGraphGenerator.generate(book.id.toString(), body.description, bookScale, genLevel, userId, userSpecifiedVoiceSpeed)
|
|
langGraphGenerator.generate(book.id.toString(), body.description, bookScale, genLevel, userId, userSpecifiedVoiceSpeed)
|
|
|
.then(() => console.log(`[LangGraph] 生成完成: bookId=${book.id}`))
|
|
.then(() => console.log(`[LangGraph] 生成完成: bookId=${book.id}`))
|
|
@@ -554,6 +575,8 @@ router.post('/books', optionalAuth, async (ctx: Context) => {
|
|
|
totalChapters: fallbackConfig.isShortArticle ? 1 : fallbackConfig.chapters,
|
|
totalChapters: fallbackConfig.isShortArticle ? 1 : fallbackConfig.chapters,
|
|
|
estimatedWords: fallbackWords,
|
|
estimatedWords: fallbackWords,
|
|
|
voiceSpeed: finalVoiceSpeed,
|
|
voiceSpeed: finalVoiceSpeed,
|
|
|
|
|
+ autoGenerateContent: autoGenContent,
|
|
|
|
|
+ autoGenerateAudio: autoGenAudio,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// 标记为 AI 正在准备中
|
|
// 标记为 AI 正在准备中
|
|
@@ -571,6 +594,22 @@ router.post('/books', optionalAuth, async (ctx: Context) => {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 用户取消自动生成文本:仅创建草稿 + 异步回填标题/规模,不启动 LangGraph 生成
|
|
|
|
|
+ if (!autoGenContent) {
|
|
|
|
|
+ if (aiPreparing) {
|
|
|
|
|
+ fillAiResultsAsync(book.id.toString(), body.description, providedTitle, providedBookScale, userId);
|
|
|
|
|
+ }
|
|
|
|
|
+ // 草稿态下不消耗音频额度,等用户手动触发时再预留
|
|
|
|
|
+ await releaseQuota(userId, fallbackCost);
|
|
|
|
|
+ console.log(`[LangGraph] 用户关闭自动生成文本,创建后保持 draft: bookId=${book.id}`);
|
|
|
|
|
+ ctx.body = {
|
|
|
|
|
+ code: 0,
|
|
|
|
|
+ message: '书籍创建成功,未启用自动生成文本',
|
|
|
|
|
+ data: { book, genStage: 'draft', mode: 'manual_content', aiPreparing },
|
|
|
|
|
+ };
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 非交互模式:需要等 AI 完成后才能确定 bookScale/genLevel 启动生成
|
|
// 非交互模式:需要等 AI 完成后才能确定 bookScale/genLevel 启动生成
|
|
|
await bookStore.update(book.id, { genStage: 'outlining' });
|
|
await bookStore.update(book.id, { genStage: 'outlining' });
|
|
|
|
|
|
|
@@ -2288,7 +2327,7 @@ router.post('/books/:id/chapters/:chapterId/regenerate-audio', requireAuth, asyn
|
|
|
const parentChapter = await prisma.bookChapter.findUnique({
|
|
const parentChapter = await prisma.bookChapter.findUnique({
|
|
|
where: { id: chapter.parentId },
|
|
where: { id: chapter.parentId },
|
|
|
});
|
|
});
|
|
|
- if (parentChapter?.audioUrl?.includes('_merged')) {
|
|
|
|
|
|
|
+ if (parentChapter?.audioUrl && (parentChapter.audioUrl.includes('/merged/chapter') || parentChapter.audioUrl.includes('_merged'))) {
|
|
|
console.log(`[RegenerateAudio] 清除父章节 ${parentChapter.id} 的合并音频(子节${chapterIdNum}已重新生成)`);
|
|
console.log(`[RegenerateAudio] 清除父章节 ${parentChapter.id} 的合并音频(子节${chapterIdNum}已重新生成)`);
|
|
|
await bookStore.updateChapterById(parentChapter.id, {
|
|
await bookStore.updateChapterById(parentChapter.id, {
|
|
|
audioUrl: '',
|
|
audioUrl: '',
|
|
@@ -2299,7 +2338,7 @@ router.post('/books/:id/chapters/:chapterId/regenerate-audio', requireAuth, asyn
|
|
|
const grandParent = await prisma.bookChapter.findUnique({
|
|
const grandParent = await prisma.bookChapter.findUnique({
|
|
|
where: { id: parentChapter.parentId },
|
|
where: { id: parentChapter.parentId },
|
|
|
});
|
|
});
|
|
|
- if (grandParent?.audioUrl?.includes('_merged')) {
|
|
|
|
|
|
|
+ if (grandParent?.audioUrl && (grandParent.audioUrl.includes('/merged/chapter') || grandParent.audioUrl.includes('_merged'))) {
|
|
|
console.log(`[RegenerateAudio] 清除祖父章节 ${grandParent.id} 的合并音频`);
|
|
console.log(`[RegenerateAudio] 清除祖父章节 ${grandParent.id} 的合并音频`);
|
|
|
await bookStore.updateChapterById(grandParent.id, {
|
|
await bookStore.updateChapterById(grandParent.id, {
|
|
|
audioUrl: '',
|
|
audioUrl: '',
|