'use strict'; const { v4: uuidv4 } = require('uuid'); const { AI_WRITING_STATUS, AI_WRITING_FAILED_STATUS, TEMP_POSITION_BASE } = require('./constants'); const { hasOwn, toInt, normalizeParentId } = require('./helpers'); const { requestFastGptTextWithApp } = require('./ai'); const { reportBusinessCall, resolveContextUserId } = require('../../services/dashboardReporter'); const { ensureInstance, ensureChapter, getInstanceChapterTree } = require('./repository'); const { ensureNoChapterCycle, reorderChapterSiblingsWithInsert, reorderChapterSiblings, } = require('./ordering'); module.exports.getInstanceChapters = async (ctx, next) => { try { const instanceId = toInt(ctx.params.instanceId); if (!instanceId) throw '缺少参数: instanceId'; await ensureInstance(ctx, instanceId); const chapters = await getInstanceChapterTree(ctx, instanceId); ctx.body = chapters; ctx.status = 200; } catch (error) { ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error === 'string' ? error : '获取章节列表失败' }; } }; module.exports.createChapter = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models } = ctx.app.fs.dc; const instanceId = toInt(ctx.params.instanceId); if (!instanceId) throw '缺少参数: instanceId'; await ensureInstance(ctx, instanceId, transaction); const { title, position, prompts } = ctx.request.body || {}; if (!String(title || '').trim()) throw '缺少参数: title'; const parentId = normalizeParentId(ctx.request.body?.parentId); await ensureNoChapterCycle(ctx, instanceId, null, parentId, transaction); const created = await models.ReportChapter.create({ instanceId, parentId, title: String(title).trim(), position: TEMP_POSITION_BASE - 1, prompts: prompts || null, status: null, }, { transaction, returning: true, }); await reorderChapterSiblingsWithInsert(ctx, instanceId, parentId, created.id, position, transaction); const fresh = await models.ReportChapter.findByPk(created.id, { transaction }); await transaction.commit(); ctx.body = fresh; ctx.status = 200; } catch (error) { await transaction.rollback(); ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error === 'string' ? error : '创建章节失败' }; } }; module.exports.updateChapter = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models } = ctx.app.fs.dc; const instanceId = toInt(ctx.params.instanceId); const chapterId = toInt(ctx.params.chapterId); if (!instanceId || !chapterId) throw '缺少参数'; const chapter = await ensureChapter(ctx, instanceId, chapterId, transaction); const body = ctx.request.body || {}; const updateData = {}; if (hasOwn(body, 'title')) updateData.title = body.title ? String(body.title).trim() : ''; if (hasOwn(body, 'prompts')) updateData.prompts = body.prompts || null; if (hasOwn(body, 'status')) updateData.status = body.status || null; const hasContentField = hasOwn(body, 'content'); const nextContent = hasContentField ? String(body.content || '') : null; const oldParentId = chapter.parentId; const nextParentId = hasOwn(body, 'parentId') ? normalizeParentId(body.parentId) : chapter.parentId; await ensureNoChapterCycle(ctx, instanceId, chapterId, nextParentId, transaction); const needMove = hasOwn(body, 'parentId') || hasOwn(body, 'position'); if (!needMove) { await models.ReportChapter.update(updateData, { where: { id: chapterId }, transaction }); if (hasContentField) { const chapterBlocks = await models.ReportBlock.findAll({ where: { instanceId, chapterId }, order: [['position', 'ASC'], ['id', 'ASC']], raw: true, transaction, }); const textBlock = chapterBlocks.find( (block) => String(block?.blockType || '').trim() === 'text' ); if (textBlock?.id) { await models.ReportBlock.update({ contentSnapshot: { ...(textBlock?.contentSnapshot || {}), content: nextContent, }, updatedAt: new Date(), }, { where: { id: textBlock.id, instanceId }, transaction, }); } else { const nextPosition = chapterBlocks.reduce((max, item) => { const pos = Number(item?.position || 0); return pos > max ? pos : max; }, 0) + 1; await models.ReportBlock.create({ instanceId, chapterId, blockType: 'text', position: nextPosition, config: { title: '正文', }, contentSnapshot: { content: nextContent, }, updatedAt: new Date(), }, { transaction, }); } } await transaction.commit(); ctx.status = 204; return; } updateData.parentId = nextParentId; updateData.position = TEMP_POSITION_BASE - 1; await models.ReportChapter.update(updateData, { where: { id: chapterId }, transaction }); if (oldParentId !== nextParentId) { await reorderChapterSiblings(ctx, instanceId, oldParentId, transaction); } const targetPosition = hasOwn(body, 'position') ? body.position : (oldParentId === nextParentId ? chapter.position : undefined); await reorderChapterSiblingsWithInsert(ctx, instanceId, nextParentId, chapterId, targetPosition, transaction); await transaction.commit(); ctx.status = 204; } catch (error) { await transaction.rollback(); ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error === 'string' ? error : '更新章节失败' }; } }; module.exports.deleteChapter = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models } = ctx.app.fs.dc; const instanceId = toInt(ctx.params.instanceId); const chapterId = toInt(ctx.params.chapterId); if (!instanceId || !chapterId) throw '缺少参数'; const chapter = await ensureChapter(ctx, instanceId, chapterId, transaction); await models.ReportChapter.destroy({ where: { id: chapterId, instanceId }, transaction, }); await reorderChapterSiblings(ctx, instanceId, chapter.parentId, transaction); await transaction.commit(); ctx.status = 204; } catch (error) { await transaction.rollback(); ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error === 'string' ? error : '删除章节失败' }; } }; module.exports.generateChapterAiContent = async (ctx, next) => { try { const { models } = ctx.app.fs.dc; const instanceId = toInt(ctx.params.instanceId); const chapterId = toInt(ctx.params.chapterId); const { prompt, blockType, config } = ctx.request.body || {}; if (!instanceId || !chapterId) throw '缺少参数'; const promptText = String(prompt || '').trim(); if (!promptText) throw '缺少参数: prompt'; const chapter = await ensureChapter(ctx, instanceId, chapterId); await models.ReportChapter.update({ prompts: promptText, status: AI_WRITING_STATUS, }, { where: { id: chapterId, instanceId }, }); ctx.body = { instanceId, chapterId, status: AI_WRITING_STATUS, message: 'AI 写作任务已开始', }; ctx.status = 202; const app = ctx.app; const logger = ctx.logger; const analyticsUserId = resolveContextUserId(ctx); const actionId = `report-chapter:${instanceId}:${chapterId}:${uuidv4()}`; const finalBlockType = String(blockType || 'ai_analysis'); const finalConfig = config || {}; setImmediate(async () => { const transaction = await app.fs.dc.orm.transaction(); try { const curReport = await app.fs.dc.models.ReportInstance.findByPk(instanceId, { transaction }); const chapters = await getInstanceChapterTree(ctx, instanceId, transaction); const aiText = await requestFastGptTextWithApp( ctx, `章节标题: ${chapter.title}\n写作提示: ${promptText}`, { actionId, userId: analyticsUserId, variables: { type: '章节内容生成', reportTitle: curReport.name, // chapters, } } ); await app.fs.dc.models.ReportBlock.destroy({ where: { instanceId, chapterId }, transaction, }); await app.fs.dc.models.ReportBlock.create({ instanceId, chapterId, blockType: finalBlockType, position: 1, config: finalConfig, contentSnapshot: { content: aiText, }, updatedAt: new Date(), }, { transaction }); await app.fs.dc.models.ReportChapter.update({ status: null, }, { where: { id: chapterId, instanceId }, transaction, }); await transaction.commit(); await reportBusinessCall({ ctx, applicationId: 'exp-report', eventId: actionId, traceId: actionId, userId: analyticsUserId, reportContext: { action: 'chapter_generation', instanceId, chapterId }, }); } catch (error) { await transaction.rollback(); logger.log(error); await app.fs.dc.models.ReportChapter.update({ status: AI_WRITING_FAILED_STATUS, }, { where: { id: chapterId, instanceId }, }); } }); } catch (error) { ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error === 'string' ? error : '触发章节 AI 写作失败' }; } };