| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991 |
- /**
- * LangGraph 书籍生成 - API 路由
- * 支持书籍创建时的预估显示
- */
- import Router from '@koa/router';
- import { Context } from 'koa';
- import { langGraphGenerator, getScaleConfig } from './index';
- import { queueService, QueueType } from '../../services/queue.service';
- import { bookStore } from './book-generator.store';
- import { estimateBookWords, estimateAudioMinutesFromWords, checkBookGenerationQuota } from '../subscription/subscription.service';
- import { optionalAuth } from '../../middleware/auth';
- import { getAllBookTypes, getDetectableTypes, getBookTypeConfig, BOOK_TYPE_CONFIG, DETECTABLE_TYPES } from './book-type-config';
- import { callLLMWithMessages, ChatMessage } from '../../services/llm';
- import { createVideoProjectFromBook, generateVideoForProject } from '../video-generator/video-generator.service';
- // 开发环境测试用户ID
- const TEST_USER_ID = '1';
- const router = new Router();
- /**
- * GET /api/book-generator/langgraph/estimate
- * 获取书籍规模预估信息
- */
- router.get('/estimate', async (ctx: Context) => {
- const { scale, userId } = ctx.query as { scale?: string; userId?: string };
-
- if (!scale) {
- ctx.status = 400;
- ctx.body = { code: 1, message: '请提供书籍规模' };
- return;
- }
- const scaleConfig = getScaleConfig(scale);
- const avgWords = Math.round((scaleConfig.wordRange.min + scaleConfig.wordRange.max) / 2);
- const audioMinutes = estimateAudioMinutesFromWords(avgWords);
- const estimatedChapters = Math.round((scaleConfig.chapterRange.min + scaleConfig.chapterRange.max) / 2);
- const result: any = {
- scale,
- scaleConfig,
- audioMinutes: {
- min: estimateAudioMinutesFromWords(scaleConfig.wordRange.min),
- max: estimateAudioMinutesFromWords(scaleConfig.wordRange.max),
- avg: audioMinutes
- },
- estimatedChapters
- };
-
- // 如果提供了 userId,同时检查用户配额
- if (userId) {
- const quotaCheck = await checkBookGenerationQuota(parseInt(userId), scale);
- result.quotaCheck = quotaCheck;
- }
-
- ctx.body = {
- code: 0,
- message: 'success',
- data: result
- };
- });
- /**
- * GET /api/book-generator/langgraph/book-types
- * 获取所有书籍类型配置(供前端显示)
- */
- router.get('/book-types', async (ctx: Context) => {
- const types = getAllBookTypes().map(t => ({
- key: t.key,
- label: t.label,
- description: t.description,
- chapters: t.chapters,
- totalWords: t.totalWords,
- chapterWords: t.chapterWords,
- sectionWords: t.sectionWords,
- structureFormat: t.structureFormat,
- readingDifficulty: t.readingDifficulty,
- isShortArticle: t.isShortArticle,
- }));
- ctx.body = {
- code: 0,
- message: 'success',
- data: types,
- };
- });
- /**
- * POST /api/book-generator/langgraph/detect-book-type
- * AI 自动检测书籍类型
- */
- router.post('/detect-book-type', async (ctx: Context) => {
- const { title, description } = ctx.request.body as {
- title?: string;
- description?: string;
- };
- if (!title) {
- ctx.status = 400;
- ctx.body = { code: 1, message: '请提供书籍标题' };
- return;
- }
- const detectableTypes = DETECTABLE_TYPES;
- // 构建类型特征描述,帮助 AI 分类
- const typeDescriptions = detectableTypes.map(key => {
- const t = BOOK_TYPE_CONFIG[key];
- return `- ${t.key}: ${t.description},${t.chapters.min}-${t.chapters.max}章,总字数${t.totalWords.min}-${t.totalWords.max}字,每章约${t.chapterWords}字,结构形式:${t.structureFormat},阅读难度:${t.readingDifficulty}`;
- }).join('\n');
- const messages: ChatMessage[] = [
- {
- role: 'system',
- content: `你是一位专业的图书分类编辑。根据用户提供的标题和描述,判断这本书最可能属于以下哪种类型:
- 可选类型:
- ${typeDescriptions}
- ## 分类规则
- 1. 分析标题中的关键词(如"科普""青少年""专业""小说"等)
- 2. 分析描述中的目标读者、写作风格、内容深度
- 3. 匹配最符合的类型
- ## 输出格式
- 必须返回 JSON,不要包含 markdown 代码块标记:
- {
- "detectedType": "类型 key",
- "confidence": 0.85,
- "reasoning": "分类理由,1-2句话"
- }
- confidence 是 0-1 的数值,表示置信程度。`,
- },
- {
- role: 'user',
- content: `标题:《${title}》\n描述:${description || '无'}`,
- },
- ];
- try {
- const response = await callLLMWithMessages(messages);
- const parsed = parseDetectResult(response);
- if (!parsed) {
- // 降级到关键词匹配
- const fallback = keywordFallback(title, description || '');
- ctx.body = {
- code: 0,
- message: 'success',
- data: {
- detectedType: fallback.detectedType,
- confidence: 0.5,
- reasoning: '基于关键词匹配(AI 解析失败,使用降级策略)',
- config: getBookTypeConfig(fallback.detectedType),
- },
- };
- return;
- }
- ctx.body = {
- code: 0,
- message: 'success',
- data: {
- detectedType: parsed.detectedType,
- confidence: parsed.confidence,
- reasoning: parsed.reasoning,
- config: getBookTypeConfig(parsed.detectedType),
- },
- };
- } catch (error: any) {
- // 最终降级:关键词匹配
- const fallback = keywordFallback(title, description || '');
- ctx.body = {
- code: 0,
- message: 'success',
- data: {
- detectedType: fallback.detectedType,
- confidence: 0.3,
- reasoning: 'LLM 调用失败,使用关键词匹配降级',
- config: getBookTypeConfig(fallback.detectedType),
- },
- };
- }
- });
- function parseDetectResult(text: string): { detectedType: string; confidence: number; reasoning: string } | null {
- try {
- const match = text.match(/\{[\s\S]*\}/);
- if (!match) return null;
- const data = JSON.parse(match[0]);
- if (!data.detectedType) return null;
- // 验证类型是否有效
- if (!DETECTABLE_TYPES.includes(data.detectedType)) {
- // 尝试模糊匹配
- const found = DETECTABLE_TYPES.find(t => data.detectedType.includes(t) || t.includes(data.detectedType));
- if (!found) return null;
- data.detectedType = found;
- }
- return {
- detectedType: data.detectedType,
- confidence: Math.min(1, Math.max(0, data.confidence || 0.5)),
- reasoning: data.reasoning || '',
- };
- } catch {
- return null;
- }
- }
- function keywordFallback(title: string, description: string): { detectedType: string } {
- const text = `${title} ${description}`.toLowerCase();
- if (text.includes('小说') || text.includes('故事') || text.includes('fiction')) {
- if (text.includes('网络') || text.includes('连载') || text.includes('修仙') || text.includes('穿越')) {
- return { detectedType: '网络小说' };
- }
- return { detectedType: '现代出版长篇小说' };
- }
- if (text.includes('科普') || text.includes('经管') || text.includes('畅销') || text.includes('通俗')) {
- return { detectedType: '科普经管畅销书' };
- }
- if (text.includes('专业') || text.includes('大学') || text.includes('研究生') || text.includes('算法') || text.includes('操作系统') || text.includes('数据库')) {
- return { detectedType: '大学专业教材' };
- }
- if (text.includes('中小学') || text.includes('初中') || text.includes('高中') || text.includes('青少年') || text.includes('儿童')) {
- return { detectedType: '中小学课本' };
- }
- if (text.includes('古典') || text.includes('章回') || text.includes('名著') || text.includes('红楼') || text.includes('西游') || text.includes('三国') || text.includes('水浒')) {
- return { detectedType: '古典名著' };
- }
- return { detectedType: '中小学课本' }; // 默认
- }
- /**
- * AI 自动检测书籍类型(后端静默调用,前端无感知)
- */
- async function autoDetectBookType(title: string, description: string): Promise<string> {
- const detectableTypes = DETECTABLE_TYPES;
- const typeDescriptions = detectableTypes.map(key => {
- const t = BOOK_TYPE_CONFIG[key];
- return `- ${t.key}: ${t.description},${t.chapters.min}-${t.chapters.max}章,总字数${t.totalWords.min}-${t.totalWords.max}字`;
- }).join('\n');
- const messages: ChatMessage[] = [
- {
- role: 'system',
- content: `你是一位专业的图书分类编辑。根据标题和描述,判断书籍属于以下哪种类型:
- 可选类型:
- ${typeDescriptions}
- 分类规则:
- 1. 分析标题中的关键词(如"科普""青少年""专业""小说"等)
- 2. 分析描述中的目标读者、写作风格、内容深度
- 3. 匹配最符合的类型
- 输出格式:只返回类型 key,不要其他内容。`,
- },
- {
- role: 'user',
- content: `标题:《${title}》\n描述:${description}`,
- },
- ];
- try {
- const response = await callLLMWithMessages(messages);
- const trimmed = response.trim();
- if (detectableTypes.includes(trimmed)) return trimmed;
- const found = detectableTypes.find(t => trimmed.includes(t) || t.includes(trimmed));
- if (found) return found;
- return keywordFallback(title, description).detectedType;
- } catch {
- return keywordFallback(title, description).detectedType;
- }
- }
- /**
- * POST /api/book-generator/langgraph/books
- * 使用 LangGraph 创建并生成书籍(异步,自动生成大纲和内容)
- * AI 根据标题+描述自动判断书籍类型
- */
- router.post('/books', async (ctx: Context) => {
- try {
- const body = ctx.request.body as {
- title: string;
- description: string;
- bookScale?: string;
- generateForeword?: boolean;
- generateAfterword?: boolean;
- };
- if (!body.title || !body.description) {
- ctx.status = 400;
- ctx.body = { code: 1, message: '书名和描述不能为空' };
- return;
- }
- // AI 自动检测书籍类型(如果前端传了 bookScale 则用前端的,否则自动检测)
- let bookScale = body.bookScale;
- if (!bookScale) {
- const detectResult = await autoDetectBookType(body.title, body.description);
- bookScale = detectResult;
- console.log(`[LangGraph] AI 自动检测类型: ${body.title} -> ${bookScale}`);
- }
- // 创建书籍(预估章节数,实际数量由AI根据字数范围分析后确定)
- const scaleConfig = getScaleConfig(bookScale);
- const estimatedChapters = Math.round((scaleConfig.chapterRange.min + scaleConfig.chapterRange.max) / 2);
- const book = await bookStore.create({
- title: body.title,
- description: body.description,
- bookScale: bookScale,
- totalChapters: estimatedChapters,
- });
- // 尝试将生成任务加入队列(队列只是为了改善用户体验)
- try {
- const jobId = await queueService.addBookGenerationTask({
- bookId: book.id,
- topic: body.description,
- bookScale,
- });
- console.log(`[LangGraph] 生成任务已加入队列: bookId=${book.id}, jobId=${jobId}`);
-
- ctx.body = {
- code: 0,
- message: '书籍创建成功,生成已开始(队列模式)',
- data: {
- book,
- jobId,
- status: 'generating',
- mode: 'queue',
- },
- };
- } catch (queueError) {
- // 队列失败时,降级为同步执行(确保核心业务不受影响)
- console.warn(`[LangGraph] 队列不可用,降级为同步执行: bookId=${book.id}`, queueError);
-
- try {
- // 同步调用生成器,等待完成
- await langGraphGenerator.generate(book.id.toString(), body.description, bookScale);
-
- console.log(`[LangGraph] 同步执行完成: bookId=${book.id}`);
-
- ctx.body = {
- code: 0,
- message: '书籍创建成功,生成已完成(同步模式)',
- data: {
- book,
- status: 'completed',
- mode: 'sync',
- },
- };
- } catch (generateError) {
- // 生成失败,更新书籍状态
- console.error(`[LangGraph] 同步执行失败: bookId=${book.id}`, generateError);
- await bookStore.update(book.id.toString(), {
- status: 'failed',
- errorMsg: generateError instanceof Error ? generateError.message : '生成失败',
- });
-
- ctx.body = {
- code: 0,
- message: '书籍创建成功,但生成失败',
- data: {
- book,
- status: 'failed',
- mode: 'sync',
- error: generateError instanceof Error ? generateError.message : '生成失败',
- },
- };
- }
- }
- } catch (error) {
- console.error('启动失败:', error);
- ctx.status = 500;
- ctx.body = {
- code: 1,
- message: error instanceof Error ? error.message : '启动失败',
- };
- }
- });
- /**
- * GET /api/book-generator/langgraph/books
- * 获取书籍列表
- * 返回:公开的书籍(有公开音频)+ 当前用户自己的书籍
- * 注意:此接口已废弃,请使用 /public-books 或 /my-books
- */
- router.get('/books', optionalAuth, async (ctx: Context) => {
- try {
- const userId = ctx.state.user?.userId;
- const userIdNum = userId ? parseInt(userId as string) : undefined;
-
- // 获取公开书籍(有公开音频的书籍)
- const publicBooks = await bookStore.getPublicBooks();
-
- // 如果用户已登录,获取用户自己的书籍
- let userBooks: any[] = [];
- if (userIdNum) {
- userBooks = await bookStore.getAllByUser(userIdNum, false);
- }
-
- // 合并并去重(按 id)
- const bookMap = new Map<string, any>();
- publicBooks.forEach(b => bookMap.set(b.id, b));
- userBooks.forEach(b => {
- if (!bookMap.has(b.id)) {
- bookMap.set(b.id, b);
- }
- });
-
- const books = Array.from(bookMap.values());
-
- ctx.body = { code: 0, message: 'success', data: { books } };
- } catch (error) {
- console.error('查询失败:', error);
- ctx.status = 500;
- ctx.body = { code: 1, message: error instanceof Error ? error.message : '查询失败' };
- }
- });
- /**
- * GET /api/book-generator/langgraph/public-books
- * 获取公开书籍列表(首页专用)
- * 只返回有公开音频的书籍
- */
- router.get('/public-books', async (ctx: Context) => {
- try {
- const publicBooks = await bookStore.getPublicBooks();
- ctx.body = { code: 0, message: 'success', data: { books: publicBooks } };
- } catch (error) {
- console.error('查询公开书籍失败:', error);
- ctx.status = 500;
- ctx.body = { code: 1, message: error instanceof Error ? error.message : '查询失败' };
- }
- });
- /**
- * GET /api/book-generator/langgraph/my-books
- * 获取当前用户自己的书籍列表(管理页专用)
- * 只返回当前用户创建的书籍
- */
- router.get('/my-books', optionalAuth, async (ctx: Context) => {
- try {
- const userId = ctx.state.user?.userId;
- if (!userId) {
- ctx.status = 401;
- ctx.body = { code: 1, message: '请先登录' };
- return;
- }
-
- const userBooks = await bookStore.getAllByUser(parseInt(userId as string), false);
- ctx.body = { code: 0, message: 'success', data: { books: userBooks } };
- } catch (error) {
- console.error('查询用户书籍失败:', error);
- ctx.status = 500;
- ctx.body = { code: 1, message: error instanceof Error ? error.message : '查询失败' };
- }
- });
- /**
- * PUT /api/book-generator/langgraph/books/:id/publish
- * 切换书籍公开状态
- */
- router.put('/books/:id/publish', optionalAuth, async (ctx: Context) => {
- try {
- const bookId = ctx.params.id;
- const newStatus = await bookStore.togglePublish(bookId);
- ctx.body = {
- code: 0,
- message: 'success',
- data: { isPublished: newStatus }
- };
- } 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
- * 获取书籍详情
- * 支持公开过滤:?filterPublic=true&userId=1
- */
- router.get('/books/:id', optionalAuth, async (ctx: Context) => {
- try {
- const bookId = ctx.params.id as string;
- const userId = ctx.state.user?.userId || TEST_USER_ID;
- const filterPublic = ctx.query.filterPublic === 'true';
-
- const book = await bookStore.getById(bookId, filterPublic, parseInt(userId as string));
- if (!book) {
- ctx.status = 404;
- ctx.body = { code: 1, message: '书籍不存在' };
- return;
- }
- ctx.body = { code: 0, message: 'success', data: { book } };
- } 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/progress
- * 获取书籍生成进度
- */
- router.get('/books/:id/progress', 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 completedChapters = book.chapters.filter((c) => c.status === 'completed').length;
- const totalChapters = book.outline?.chapters?.length || book.totalChapters || 0;
- // 如果有大纲,使用大纲章节数计算进度
- let progress = book.progress;
- if (totalChapters > 0 && book.status !== 'completed') {
- progress = Math.round((completedChapters / totalChapters) * 100);
- }
- ctx.body = {
- code: 0,
- message: 'success',
- data: {
- bookId,
- status: book.status,
- progress,
- completedChapters,
- totalChapters,
- },
- };
- } catch (error) {
- console.error('查询进度失败:', error);
- ctx.status = 500;
- ctx.body = { code: 1, message: error instanceof Error ? error.message : '查询失败' };
- }
- });
- /**
- * DELETE /api/book-generator/langgraph/books/:id
- * 删除书籍
- */
- router.delete('/books/:id', async (ctx: Context) => {
- try {
- const bookId = ctx.params.id as string;
- await bookStore.delete(bookId);
- ctx.body = { code: 0, message: '删除成功' };
- } 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/generate
- * 对已有书籍使用 LangGraph 生成
- */
- router.post('/books/:id/generate', async (ctx: Context) => {
- try {
- const bookId = ctx.params.id as string;
- console.log(`[LangGraph Generate] 收到请求 bookId=${bookId}`);
-
- const body = (ctx.request.body || {}) as { bookScale?: string };
- const book = await bookStore.getById(bookId);
- console.log(`[LangGraph Generate] book对象:`, book ? '存在' : 'null');
- if (!book) {
- ctx.status = 404;
- ctx.body = { code: 1, message: '书籍不存在' };
- return;
- }
- // 防止重复生成:如果书籍正在生成中,拒绝请求
- if (book.status === 'generating') {
- console.log(`[LangGraph Generate] 书籍正在生成中,拒绝重复请求 bookId=${bookId}`);
- ctx.status = 400;
- ctx.body = {
- code: 1,
- message: '书籍正在生成中,请勿重复提交',
- data: {
- bookId,
- status: book.status,
- progress: book.progress,
- }
- };
- return;
- }
- // 优先使用请求传入的 scale,否则使用书籍保存的 scale,最后默认标准教程
- const bookScale = body.bookScale || book.bookScale || '标准教程';
- // 尝试将生成任务加入队列(队列只是为了改善用户体验)
- try {
- const jobId = await queueService.addBookGenerationTask({
- bookId,
- topic: book.description,
- bookScale,
- });
-
- console.log(`[LangGraph] 生成任务已加入队列: bookId=${bookId}, jobId=${jobId}`);
-
- ctx.body = {
- code: 0,
- message: 'LangGraph 生成任务已启动(队列模式)',
- data: {
- bookId,
- jobId,
- status: 'queued',
- mode: 'queue',
- },
- };
- } catch (queueError) {
- // 队列失败时,降级为同步执行(确保核心业务不受影响)
- console.warn(`[LangGraph] 队列不可用,降级为同步执行: bookId=${bookId}`, queueError);
-
- try {
- // 同步调用生成器,等待完成
- await langGraphGenerator.generate(bookId, book.description, bookScale);
-
- console.log(`[LangGraph] 同步执行完成: bookId=${bookId}`);
-
- ctx.body = {
- code: 0,
- message: 'LangGraph 生成任务已完成(同步模式)',
- data: {
- bookId,
- status: 'completed',
- mode: 'sync',
- },
- };
- } catch (generateError) {
- // 生成失败,更新书籍状态
- console.error(`[LangGraph] 同步执行失败: bookId=${bookId}`, generateError);
- await bookStore.update(bookId, {
- status: 'failed',
- errorMsg: generateError instanceof Error ? generateError.message : '生成失败',
- });
-
- ctx.body = {
- code: 0,
- message: 'LangGraph 生成任务失败',
- data: {
- bookId,
- status: 'failed',
- mode: 'sync',
- error: generateError instanceof Error ? generateError.message : '生成失败',
- },
- };
- }
- }
- } 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/audio
- * 批量生成书籍所有小节的音频
- */
- router.post('/books/:id/audio', async (ctx: Context) => {
- try {
- const bookId = ctx.params.id as string;
- const { voiceId = 'cherry' } = ctx.request.body as { voiceId?: string };
- const book = await bookStore.getById(bookId);
- if (!book) {
- ctx.status = 404;
- ctx.body = { code: 1, message: '书籍不存在' };
- return;
- }
- // 获取所有小节 (level=3)
- const chapters = await bookStore.getChapterTree(bookId);
- const subsections = chapters.filter((c: any) => c.level === 3);
- // 检查内容状态
- const subsectionsWithContent = subsections.filter((c: any) => c.content && c.contentStatus === 'completed');
- if (subsections.length === 0) {
- ctx.body = { code: 1, message: '没有小节' };
- return;
- }
- if (subsectionsWithContent.length === 0) {
- ctx.body = {
- code: 1,
- message: '没有内容生成完成的小节,请先生成内容',
- data: {
- totalSubsections: subsections.length,
- completedSubsections: 0,
- }
- };
- return;
- }
- if (subsections.length !== subsectionsWithContent.length) {
- console.log(`[Audio] 书籍 ${bookId} 共有 ${subsections.length} 个小节,其中 ${subsectionsWithContent.length} 个内容已生成完成`);
- }
- // 异步生成所有小节音频
- for (const sub of subsectionsWithContent) {
- bookStore.generateChapterAudioById(sub.id, book.userId || 1).catch(err => {
- console.error(`[Audio] 小节${sub.number}音频生成失败:`, err);
- });
- }
- ctx.body = {
- code: 0,
- message: '音频生成任务已启动',
- data: {
- totalSubsections: subsectionsWithContent.length,
- taskId: `audio_${bookId}_${Date.now()}`,
- },
- };
- } 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/failed-chapters
- * 获取生成失败的小节列表
- */
- router.get('/books/:id/failed-chapters', 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);
- const failedSubsections = chapters.filter(c => c.status === 'failed');
- ctx.body = {
- code: 0,
- message: 'success',
- data: {
- bookId,
- failedCount: failedSubsections.length,
- failedChapters: failedSubsections.map(c => ({
- id: c.id,
- number: c.number,
- title: c.title,
- level: c.level,
- errorMsg: c.errorMsg,
- parentId: c.parentId,
- })),
- },
- };
- } 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/resume
- * 从断点处继续生成(重试失败的小节)
- */
- router.post('/books/:id/resume', 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.status === 'completed') {
- ctx.body = { code: 1, message: '书籍已生成完成,无需继续' };
- return;
- }
- // 获取失败的小节
- const chapters = await bookStore.getChapterTree(bookId);
- const failedSubsections = chapters.filter(c => c.status === 'failed');
- if (failedSubsections.length === 0) {
- ctx.body = { code: 1, message: '没有失败的小节需要重试' };
- return;
- }
- // 重置失败小节的状态为 pending
- for (const sub of failedSubsections) {
- await bookStore.updateChapterById(sub.id, {
- status: 'pending',
- errorMsg: null,
- content: null,
- });
- }
- // 将续生成任务加入队列
- const jobId = await queueService.addBookGenerationTask({
- bookId,
- topic: book.description || book.title,
- bookScale: book.bookScale || '标准教程',
- });
-
- console.log(`[LangGraph] 续生成任务已加入队列: bookId=${bookId}, jobId=${jobId}`);
- ctx.body = {
- code: 0,
- message: `已重启生成,将重试 ${failedSubsections.length} 个失败的小节`,
- data: {
- bookId,
- retryCount: failedSubsections.length,
- status: 'resuming',
- },
- };
- } 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/retry-chapter
- * 单独重试某个失败的小节
- */
- router.post('/books/:id/retry-chapter', async (ctx: Context) => {
- try {
- const bookId = ctx.params.id as string;
- const { chapterId } = ctx.request.body as { chapterId: number };
- if (!chapterId) {
- ctx.status = 400;
- ctx.body = { code: 1, message: '请提供章节 ID' };
- return;
- }
- // 获取所有章节查找指定的章节
- const chapters = await bookStore.getChapterTree(bookId);
- const chapter = chapters.find(c => c.id === chapterId);
- if (!chapter) {
- ctx.status = 404;
- ctx.body = { code: 1, message: '章节不存在' };
- return;
- }
- // 重置章节状态
- await bookStore.updateChapterById(chapterId, {
- status: 'pending',
- errorMsg: null,
- content: null,
- });
- ctx.body = {
- code: 0,
- message: '章节已重置为待生成状态',
- data: { chapterId },
- };
- } 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/videos
- * 批量生成书籍所有章节视频
- */
- router.post('/books/:id/videos', 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;
- }
- // 获取所有小节 (level=3)
- const chapters = await bookStore.getChapterTree(bookId);
- const subsections = chapters.filter((c: any) => c.level === 3);
- // 过滤出有音频的小节(audioUrl存在且不为空)
- const subsectionsWithAudio = subsections.filter((c: any) => c.audioUrl && c.audioUrl !== '');
- if (subsections.length === 0) {
- ctx.body = { code: 1, message: '没有小节' };
- return;
- }
- if (subsectionsWithAudio.length === 0) {
- ctx.body = {
- code: 1,
- message: '没有音频生成完成的小节,请先生成音频',
- data: {
- totalSubsections: subsections.length,
- audioCompletedSubsections: 0,
- }
- };
- return;
- }
- if (subsections.length !== subsectionsWithAudio.length) {
- console.log(`[Video] 书籍 ${bookId} 共有 ${subsections.length} 个小节,其中 ${subsectionsWithAudio.length} 个音频已生成完成`);
- }
- // 异步生成所有章节视频
- const taskId = `video_${bookId}_${Date.now()}`;
- console.log(`[Video] 开始批量生成视频: bookId=${bookId}, taskId=${taskId}, 总数=${subsectionsWithAudio.length}`);
- for (const sub of subsectionsWithAudio) {
- // 异步处理每个章节的视频生成
- (async () => {
- try {
- console.log(`[Video] 开始生成章节 ${sub.number} 的视频: ${sub.title}`);
-
- // 从书籍章节创建视频项目
- const project = await createVideoProjectFromBook(
- parseInt(bookId),
- sub.id,
- book.userId || 1
- );
- if (!project) {
- console.error(`[Video] 章节 ${sub.number} 视频项目创建失败`);
- return;
- }
- console.log(`[Video] 章节 ${sub.number} 视频项目创建成功: projectId=${project.id}`);
- // 生成视频
- const result = await generateVideoForProject(project.id);
- if (result.success && result.outputUrl) {
- // 更新章节的视频URL
- await bookStore.updateChapterById(sub.id, {
- videoUrl: result.outputUrl,
- videoDuration: result.duration,
- });
- console.log(`[Video] 章节 ${sub.number} 视频生成成功: ${result.outputUrl}`);
- } else {
- console.error(`[Video] 章节 ${sub.number} 视频生成失败:`, result.error);
- }
- } catch (error) {
- console.error(`[Video] 章节 ${sub.number} 视频生成异常:`, error);
- }
- })();
- }
- ctx.body = {
- code: 0,
- message: '视频生成任务已启动',
- data: {
- taskId,
- totalChapters: subsectionsWithAudio.length,
- totalSubsections: subsections.length,
- },
- };
- } catch (error) {
- console.error('批量生成视频失败:', error);
- ctx.status = 500;
- ctx.body = { code: 1, message: error instanceof Error ? error.message : '批量生成视频失败' };
- }
- });
- export default router;
|