'use strict'; const superagent = require('superagent'); const moment = require('moment'); const { getQuotaService, trackGenerateUsage: sharedTrackGenerateUsage, recordAiQueryLog, } = require('../utils/aiUsage'); const { respondQuotaLimited } = require('../utils/quotaResponse'); const { reportBusinessCall } = require('../services/dashboardReporter'); const TENDER_STATUS_ENUM = { 'created': '刚创建', 'chaptersGenerating': '预览目录生成中', 'chaptersGenerateSuccess': '预览目录生成成功', 'chaptersGenerateFailed': '预览目录生成失败', 'chaptersCreated': '已确认创建目录', 'contentGenerating': '内容生成中', 'contentGenerateSuccess': '内容生成成功', 'contentGenerateFailed': '内容生成失败', } const trackGenerateUsage = (ctx, payload) => sharedTrackGenerateUsage(ctx, { ...payload, docKind: getQuotaService(ctx)?.DOC_KIND.PLAN, analyticsApplicationId: 'stable-industry', analyticsAppKey: ctx.app.fs.config.fastGpt?.solutionAppKey, }); const resolveCurrentRegisterSource = async (ctx, preferredUserId = null) => { ctx.fs = ctx.fs || {}; if (ctx.fs.currentRegisterSource !== undefined) { return ctx.fs.currentRegisterSource; } const fromRequestBody = String(ctx.request?.body?.registerSource || '').trim(); if (fromRequestBody) { ctx.fs.currentRegisterSource = fromRequestBody; return fromRequestBody; } const userInfo = ctx.fs?.curUser?.userInfo || {}; const fromUserInfo = String( userInfo.registerSource || userInfo.register_source || userInfo.source || '' ).trim(); if (fromUserInfo) { ctx.fs.currentRegisterSource = fromUserInfo; return fromUserInfo; } const internalUserId = preferredUserId || ctx.fs?.userIdMapping?.internalUserId || ctx.request?.body?.creator || ctx.request?.body?.userId || ctx.request?.body?.userid || null; if (!internalUserId) { ctx.fs.currentRegisterSource = ''; return ''; } const normalizedUserId = Number(internalUserId); if (!Number.isFinite(normalizedUserId) || normalizedUserId <= 0) { ctx.fs.currentRegisterSource = ''; return ''; } const { models } = ctx.app.fs.dc; const tenderUser = await models.TenderUser.findOne({ attributes: ['registerSource'], where: { id: normalizedUserId }, raw: true, }); const registerSource = String(tenderUser?.registerSource || '').trim(); ctx.fs.currentRegisterSource = registerSource; return registerSource; }; const withRegisterSourceVariables = async (ctx, payload = {}, preferredUserId = null) => { const registerSource = await resolveCurrentRegisterSource(ctx, preferredUserId); return { ...payload, variables: { ...(payload.variables || {}), registerSource, }, }; }; module.exports.getSolutionList = async (ctx, next) => { try { const { models, ORM: { Op } } = ctx.app.fs.dc; const { page, pageSize, creator, name } = ctx.request.query; const where = {}; if (creator) { where.creator = creator; } if (name) { where.name = { [Op.like]: `%${name}%` }; } const options = { where, order: [['updateAt', 'DESC']], raw: true, }; if (page && pageSize) { options.offset = (page - 1) * pageSize; options.limit = parseInt(pageSize); } const list = await models.Solutions.findAndCountAll(options); ctx.body = list; ctx.status = 200; } catch (error) { ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : '获取方案列表失败' }; } } module.exports.createSolution = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models } = ctx.app.fs.dc; const quotaService = getQuotaService(ctx); const { name, industry, messages, projectInfo, requirements, creator, curIp, confirmCharge, confirmToken, } = ctx.request.body; if (!name) { throw '缺少参数' }; const effectiveCreator = creator || ctx.request.body?.userId || ctx.request.body?.userid || ctx.fs?.userIdMapping?.internalUserId; const userId = quotaService?.normalizeUserId?.(effectiveCreator); let quotaCheck = { passed: true }; if (userId) { quotaCheck = await quotaService.checkCreateQuota({ transaction, userId, docKind: quotaService.DOC_KIND.PLAN, confirmCharge, confirmToken, externalUserId: ctx?.fs?.userIdMapping?.externalUserId || '', }); if (quotaCheck?.passed === false) { await transaction.rollback(); respondQuotaLimited(ctx, { message: quotaCheck.message, limitType: 'create', docKind: 'plan', code: quotaCheck.code, confirmToken: quotaCheck.confirmToken || null, confirmExpireAt: quotaCheck.confirmExpireAt || null, }); return; } } // 新增方案 const solution = await models.Solutions.create( { name, industry, messages, projectInfo, requirements, status: 'created', creator: effectiveCreator, createAt: moment(), updateAt: moment(), }, { returning: true, transaction }, ); if (userId) { const createShouldCharge = quotaCheck?.chargeRequired === true; const createAmountYuan = createShouldCharge ? Number(quotaCheck?.chargeAmountYuan || quotaService.createChargeYuan || 5) : 0; const createRequestId = createShouldCharge ? quotaService.createCreateChargeRequestId({ userId, docKind: quotaService.DOC_KIND.PLAN, docId: solution.id, }) : null; const createEventId = await quotaService.recordCreateEvent({ transaction, userId, docKind: quotaService.DOC_KIND.PLAN, docId: solution.id, source: 'manual', billingStatus: createShouldCharge ? quotaService.BILLING_STATUS.PENDING : quotaService.BILLING_STATUS.SKIP, amountYuan: createAmountYuan, requestId: createRequestId, }); if (createShouldCharge) { if (!createEventId) { throw '创建计费流水写入失败,请重试'; } const aideductUserId = await quotaService.resolveChargeUserId({ transaction, userId, externalUserId: ctx?.fs?.userIdMapping?.externalUserId || '', }); const settledStatus = await quotaService.settleGenerateBilling({ eventId: createEventId, aideductUserId, amountYuan: createAmountYuan, requestId: createRequestId, logger: ctx.logger, }); if (settledStatus !== quotaService.BILLING_STATUS.CONFIRMED) { throw '扣费失败,请稍后重试'; } } } // 新增日志记录 await recordAiQueryLog(ctx, { userId: effectiveCreator, ipAddress: curIp, feature: 'AI方案生成', transaction, }); await transaction.commit(); ctx.body = solution; ctx.status = 200; } catch (error) { await transaction.rollback(); ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : '新增方案失败' }; } } module.exports.modifySolution = async (ctx, next) => { try { const { models } = ctx.app.fs.dc; const solutionId = ctx.params.solutionId; const body = ctx.request.body; if (!solutionId) { throw '缺少参数' }; await models.Solutions.update( { ...body, updateAt: moment() }, { where: { id: solutionId } } ); ctx.status = 204; } catch (error) { ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : '修改方案失败' }; } } module.exports.delSolution = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models } = ctx.app.fs.dc; const solutionId = ctx.params.solutionId; if (!solutionId) { throw '缺少参数' }; await models.SolutionChapterVersions.destroy({ where: { solutionId }, transaction }); await models.SolutionChapters.destroy({ where: { solutionId }, transaction }); await models.Solutions.destroy({ where: { id: solutionId }, 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.generateChapters = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); const { models, ORM: { Op } } = ctx.app.fs.dc; const { solutionId } = ctx.params; try { const { apiUrl: fastGptApiUrl, solutionAppKey } = ctx.app.fs.config.fastGpt; const { projectInfo } = ctx.request.body; if (!solutionId || !projectInfo) { throw '缺少参数' }; const solution = await models.Solutions.findOne({ where: { id: solutionId }, raw: true }); await models.Solutions.update( { status: 'chaptersGenerating', projectInfo, updateAt: moment() }, { where: { id: solutionId } } ); let projectInfoText = ''; if (Array.isArray(projectInfo) && projectInfo.length > 0) { projectInfoText = projectInfo.map(item => item?.title + ':' + item?.content).join('\n'); } const res = await superagent .post(`${fastGptApiUrl}/api/v1/chat/completions`) .send(await withRegisterSourceVariables(ctx, { "stream": false, "detail": true, "variables": { "generation_type": '目录生成' }, "messages": [ { "role": "user", "content": [ { "type": "text", "text": `项目名称:${solution.name}\n所属行业:${solution.industry}\n\n${projectInfoText}` }, ] } ] }, solution?.creator)) .set({ Authorization: `Bearer ${solutionAppKey}`, "Content-Type": "application/json", }) const resContent = res.body.choices[0].message.content; const previewChapters = JSON.parse(resContent); await models.Solutions.update( { previewChapters, status: 'chaptersGenerateSuccess', updateAt: moment() }, { where: { id: solutionId }, transaction }, ); await transaction.commit(); await trackGenerateUsage(ctx, { creator: solution?.creator, docId: solutionId, responseBody: res.body, }); ctx.status = 200; ctx.body = previewChapters; } catch (error) { await transaction.rollback(); await models.Solutions.update( { status: 'chaptersGenerateFailed', updateAt: moment() }, { where: { id: solutionId } }, ); ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : '目录生成失败' }; } } module.exports.sortChapters = async (ctx, next) => { try { const { models, ORM: { Op } } = ctx.app.fs.dc; const { apiUrl: fastGptApiUrl, solutionAppKey } = ctx.app.fs.config.fastGpt; const { solutionId } = ctx.params; const previewChapters = ctx.request.body; if (!solutionId || !previewChapters) { throw '参数错误' }; const solution = await models.Solutions.findOne({ attributes: ['id', 'creator'], where: { id: solutionId }, raw: true, }); const res = await superagent .post(`${fastGptApiUrl}/api/v1/chat/completions`) .send(await withRegisterSourceVariables(ctx, { "stream": false, "detail": true, "variables": { "generation_type": "整理预览目录" }, "messages": [ { "role": "user", "content": JSON.stringify(previewChapters) } ] }, solution?.creator)) .set({ Authorization: `Bearer ${solutionAppKey}`, "Content-Type": "application/json", }) const resContent = res.body.choices[0].message.content; const newPreviewChapters = JSON.parse(resContent); await models.Solutions.update( { previewChapters: newPreviewChapters, updateAt: moment() }, { where: { id: solutionId } }, ); await trackGenerateUsage(ctx, { creator: solution?.creator, docId: solutionId, responseBody: res.body, }); ctx.status = 200; ctx.body = newPreviewChapters; } catch (error) { ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : '整理章节失败' }; } } module.exports.createChapters = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models, ORM: { Op } } = ctx.app.fs.dc; const { solutionId } = ctx.params; const { previewChapters, targetPages, targetWords } = ctx.request.body; if (!solutionId || !previewChapters) { throw '参数错误' }; // 删除原目录和内容 await models.SolutionChapterVersions.destroy({ where: { solutionId: Number(solutionId) }, transaction }); await models.SolutionChapters.destroy({ where: { solutionId: Number(solutionId) }, transaction }); // 分层插入章节数据 const insertChaptersRecursively = async (chapters, parentId = null, sortOrder = 0, level = 0) => { for (let i = 0; i < chapters.length; i++) { const chapter = chapters[i]; // 插入当前章节 const insertedChapter = await models.SolutionChapters.create({ solutionId: Number(solutionId), title: chapter.name, targetPages: level === 0 ? targetPages : null, targetWords: level === 0 ? targetWords : null, parentId, sortOrder: sortOrder + i, level }, { transaction }); // 如果有子章节,递归插入 if (chapter.child && chapter.child.length > 0) { await insertChaptersRecursively(chapter.child, insertedChapter.id, 0, level + 1); } } }; await insertChaptersRecursively(previewChapters); await models.Solutions.update( { previewChapters: previewChapters, status: 'chaptersCreated', updateAt: moment() }, { where: { id: solutionId }, 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.getSolutionChapters = async (ctx, next) => { try { const { models } = ctx.app.fs.dc; const solutionId = ctx.params.solutionId; if (!solutionId) { throw '缺少参数: solutionId' }; const chapters = await models.SolutionChapters.findAll({ where: { solutionId: solutionId }, order: [['id', 'ASC']], include: [ { model: models.SolutionChapterVersions, where: { isCurrent: true, }, required: false, } ], }); ctx.body = chapters; ctx.status = 200; } catch (error) { ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : '获取方案章节失败' }; } } module.exports.addSolutionChapters = async (ctx, next) => { try { const { models } = ctx.app.fs.dc; const { solutionId } = ctx.params; const body = ctx.request.body; if (!solutionId || !body?.title) { throw '缺少参数' }; body.solutionId = Number(solutionId); // 查询同级的最后一个章节 const lastChild = await models.SolutionChapters.findOne({ where: { solutionId, parentId: body.parentId || null }, attributes: ['sortOrder'], order: [['sortOrder', 'DESC']], raw: true, }); if (lastChild) { body.sortOrder = lastChild.sortOrder + 1; } else { body.sortOrder = 0; } await models.SolutionChapters.create(body); ctx.status = 204; } catch (error) { ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : '添加章节失败' }; } } module.exports.modifySolutionChapters = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models, ORM: { Op } } = ctx.app.fs.dc; const { solutionId, chapterId } = ctx.params; const body = ctx.request.body; if (!solutionId || !chapterId) { throw '缺少参数' }; // 检查是否需要修改章节顺序或父章节 if (body.sortOrder !== undefined || body.parentId !== undefined) { // 获取目标章节信息 const targetChapter = await models.SolutionChapters.findOne({ where: { id: chapterId, solutionId }, attributes: ['parentId', 'sortOrder'], raw: true, transaction }); if (!targetChapter) { throw '章节未找到'; } const { parentId: originalParentId, sortOrder: originalSortOrder } = targetChapter; const newParentId = body.parentId !== undefined ? body.parentId : originalParentId; const newSortOrder = body.sortOrder !== undefined ? body.sortOrder : originalSortOrder; //[updates][按固定顺序串行更新,避免并发更新触发数据库死锁] const applySortUpdatesSequentially = async (updates = []) => { for (const update of updates) { await models.SolutionChapters.update( { sortOrder: update.sortOrder, updateAt: moment() }, { where: { id: update.id }, transaction } ); } }; // 如果父章节改变了,需要重新计算排序 if (newParentId !== originalParentId) { // 更新章节的父章节 await models.SolutionChapters.update( { parentId: newParentId }, { where: { id: chapterId }, transaction } ); // 批量更新原父章节下的所有章节排序 const originalSiblingChapters = await models.SolutionChapters.findAll({ attributes: ['id', 'sortOrder'], where: { solutionId, parentId: originalParentId, }, order: [['sortOrder', 'ASC']], raw: true, transaction }); // 重新计算原父章节下剩余章节的排序 const originalUpdates = originalSiblingChapters .filter(chapter => chapter.id !== parseInt(chapterId)) .map((chapter, index) => ({ id: chapter.id, sortOrder: index })); // 批量更新原父章节下剩余章节的排序 if (originalUpdates.length > 0) { await applySortUpdatesSequentially(originalUpdates); } // 批量更新新父章节下的所有章节排序 const newSiblingChapters = await models.SolutionChapters.findAll({ attributes: ['id', 'sortOrder'], where: { solutionId, parentId: newParentId, id: { [Op.ne]: parseInt(chapterId) } }, order: [['sortOrder', 'ASC']], raw: true, transaction }); // 创建移动章节对象并插入到新位置 const movingChapter = { id: parseInt(chapterId) }; newSiblingChapters.splice(newSortOrder, 0, movingChapter); // 准备批量更新数据 const newUpdates = newSiblingChapters.map((chapter, index) => ({ id: chapter.id, sortOrder: index })); // 批量更新新父章节下所有章节的排序 await applySortUpdatesSequentially(newUpdates); } else { // 父章节未改变,只修改排序 // 获取所有兄弟章节 const siblingChapters = await models.SolutionChapters.findAll({ attributes: ['id', 'sortOrder'], where: { solutionId, parentId: originalParentId, }, order: [['sortOrder', 'ASC']], raw: true, transaction }); // 从当前位置移除目标章节并将其插入到新位置 const currentChapterIndex = siblingChapters.findIndex(chapter => chapter.id === parseInt(chapterId)); const [chapterToMove] = siblingChapters.splice(currentChapterIndex, 1); siblingChapters.splice(newSortOrder, 0, chapterToMove); // 准备批量更新数据 const updates = siblingChapters.map((chapter, index) => ({ id: chapter.id, sortOrder: index })); // 批量更新所有兄弟章节的排序 await applySortUpdatesSequentially(updates); } } else { // 其它修改(不涉及排序和父章节) await models.SolutionChapters.update( { ...body, updateAt: moment() }, { where: { id: chapterId, solutionId }, 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.delSolutionChapters = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models, ORM: { Op } } = ctx.app.fs.dc; const { solutionId, chapterId } = ctx.params; if (!solutionId || !chapterId) { throw '缺少参数' }; // 获取所有子章节ID async function getChildIds(parentId, allChildIds = []) { const children = await models.SolutionChapters.findAll({ attributes: ["id"], // 只查询 id 字段 where: { parentId }, }); if (children.length > 0) { const childIds = children.map(child => child.id); allChildIds.push(...childIds); // 对每个子节点递归查找其子节点 for (const childId of childIds) { await getChildIds(childId, allChildIds); } } return allChildIds; } // 获取目标节点及其所有子节点的 ID const allIdsToDelete = [Number(chapterId)]; // 包含目标节点本身 const childIds = await getChildIds(chapterId); allIdsToDelete.push(...childIds); await models.SolutionChapterVersions.destroy({ where: { chapterId: { [Op.in]: allIdsToDelete }, solutionId }, transaction }); await models.SolutionChapters.destroy({ where: { id: { [Op.in]: allIdsToDelete }, solutionId }, 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.startGenerateContent = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models, ORM: { Op } } = ctx.app.fs.dc; const quotaService = getQuotaService(ctx); const { solutionId } = ctx.params; const { targetCharts, targetTables, requirements, confirmCharge, confirmToken, } = ctx.request.body; if (!solutionId) { throw '缺少参数' }; if (requirements) { await models.Solutions.update( { requirements, updateAt: moment() }, { where: { id: solutionId }, transaction } ); } if (targetCharts || targetTables) { await models.SolutionChapters.update( { targetCharts, targetTables, updateAt: moment() }, { where: { solutionId }, transaction } ); } const solution = await models.Solutions.findOne({ where: { id: solutionId }, raw: true, transaction }); const allChapters = await models.SolutionChapters.findAll({ where: { solutionId }, raw: true, transaction }); const shouldCountFullRegenerateRewrite = String(solution?.status || '') === 'contentGenerateSuccess'; const userId = quotaService?.normalizeUserId?.(solution?.creator); let rewriteOverLimit = false; if (shouldCountFullRegenerateRewrite && userId) { const sectionCount = allChapters.filter((chapter) => Number(chapter?.level) === 1).length; const consumeRewriteCount = Math.max(1, sectionCount || 0); const rewriteQuotaCheck = await quotaService.checkAndRecordRewrite({ transaction, userId, docId: Number(solutionId), docKind: quotaService.DOC_KIND.PLAN, chapterKey: `full_content:${solutionId}`, action: 'rewrite_full_content', consumeCount: consumeRewriteCount, confirmCharge, confirmToken, externalUserId: ctx?.fs?.userIdMapping?.externalUserId || '', }); if (rewriteQuotaCheck?.passed === false) { await transaction.rollback(); respondQuotaLimited(ctx, { message: rewriteQuotaCheck.message, limitType: 'rewrite', docKind: 'plan', code: rewriteQuotaCheck.code, confirmToken: rewriteQuotaCheck.confirmToken || null, confirmExpireAt: rewriteQuotaCheck.confirmExpireAt || null, }); return; } rewriteOverLimit = rewriteQuotaCheck?.overLimit === true; } generateAllContent(ctx, solution, allChapters, { rewriteOverLimit }); 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 : '启动内容生成任务失败' }; } } const generateAllContent = async (ctx, solution, allChapters, options = {}) => { const { models } = ctx.app.fs.dc; const { apiUrl: fastGptApiUrl, solutionAppKey } = ctx.app.fs.config.fastGpt; const rewriteOverLimit = options?.rewriteOverLimit === true; const registerSource = await resolveCurrentRegisterSource(ctx, solution?.creator); try { // 更新方案状态为生成 await models.Solutions.update( { status: 'contentGenerating', updateAt: moment() }, { where: { id: solution.id } } ); // 构建章节映射,方便查找父级章 const chapterMap = new Map(); allChapters.forEach(chapter => { chapterMap.set(chapter.id, chapter); }); // 筛选出没有子章节的叶子章节 const leafChapters = allChapters.filter(chapter => { // 检查是否有其他章节parentId 等于当前章节id return !allChapters.some(c => c.parentId === chapter.id); }); console.log(`开始生成内容,${leafChapters.length} 个叶子章节`); // 构建完整章节路径的辅助函 const buildChapterPath = (chapter) => { const path = []; let current = chapter; while (current) { path.unshift(current.title); current = current.parentId ? chapterMap.get(current.parentId) : null; } return path.join('-'); }; // 构建完整目录结构文本 const buildChapterStructure = (chapters) => { const chaptersByParent = new Map(); // parentId 分组 chapters.forEach(chapter => { const parentId = chapter.parentId || 'root'; if (!chaptersByParent.has(parentId)) { chaptersByParent.set(parentId, []); } chaptersByParent.get(parentId).push(chapter); }); // sortOrder 排序 chaptersByParent.forEach(siblings => { siblings.sort((a, b) => a.sortOrder - b.sortOrder); }); const lines = []; const buildLevel = (parentId, prefix = '') => { const children = chaptersByParent.get(parentId) || []; children.forEach((chapter, index) => { const number = prefix ? `${prefix}.${index + 1}` : `${index + 1}`; lines.push(`${number} ${chapter.title}`); // 递归处理子章 buildLevel(chapter.id, number); }); }; buildLevel('root'); return lines.join('\n'); }; const chaptersText = buildChapterStructure(allChapters); let lastContent = ''; let hasChapterError = false; const usageTrackQueue = []; const enqueueGenerateUsage = (payload) => { usageTrackQueue.push(payload); }; // 依次生成每个叶子章节的内容? for (const chapter of leafChapters) { try { const chapterPath = buildChapterPath(chapter); console.log(`solution ${solution.id} 正在生成章节: ${chapterPath}`); // 构建发送的内容 const variables = { generation_type: '内容生成', lastContent, chapters: chaptersText, registerSource, }; if (solution.projectInfo && solution.projectInfo.length > 0) { variables.projectInfo = solution.projectInfo.map(item => item?.title + ':' + item?.content).join('\n'); } if (solution.requirements) { variables.globalRequirements = solution.requirements; } if (chapter.requirements) { variables.chapterRequirements = chapter.requirements; } if (chapter.writingDirection) { variables.chapterWritingDirection = chapter.writingDirection; } let sendText = `编写章节:${chapterPath}`; if (chapter.targetCharts) { sendText += `\n图表数:${chapter.targetCharts}`; } if (chapter.targetTables) { sendText += `\n表格数:${chapter.targetTables}`; } if (chapter.targetSegmentsWords) { sendText += `\n目标字数:约${chapter.targetSegmentsWords}字`; } // 调用 FastGPT API 生成章节内容 const res = await superagent .post(`${fastGptApiUrl}/api/v1/chat/completions`) .send(await withRegisterSourceVariables(ctx, { "stream": false, "detail": true, "variables": variables, "messages": [ { "role": "user", "content": [ { "type": "text", "text": sendText }, ] } ] }, solution?.creator)) .set({ Authorization: `Bearer ${solutionAppKey}`, "Content-Type": "application/json", }); const content = res.body.choices[0].message.content; lastContent = content; // 检查该章节是否已有版本记录 const existingVersion = await models.SolutionChapterVersions.findOne({ where: { solutionId: solution.id, chapterId: chapter.id }, attributes: ['version'], order: [['version', 'DESC']], raw: true }); const newVersion = existingVersion ? existingVersion.version + 1 : 1; // 如果存在旧版本,将所有旧版本标记为非当前版本 if (existingVersion) { await models.SolutionChapterVersions.update( { isCurrent: false }, { where: { solutionId: solution.id, chapterId: chapter.id, isCurrent: true, } } ); } // 存入新版 await models.SolutionChapterVersions.create({ solutionId: solution.id, chapterId: chapter.id, version: newVersion, isCurrent: true, content: content }); enqueueGenerateUsage({ creator: solution?.creator, docId: solution?.id, chapterKey: `chapter:${chapter.id}`, responseBody: res.body, ignoreCreateFree: rewriteOverLimit, allowChapterFirstFree: !rewriteOverLimit, }); console.log(`solution ${solution.id} 章节生成成功: ${chapterPath}, 版本: ${newVersion}`); } catch (chapterError) { hasChapterError = true; console.error(`solution ${solution.id} 章节生成失败: ${chapter.title}`, chapterError); // 继续处理下一个章节,不中断整个流 } } if (hasChapterError) { await models.Solutions.update( { status: 'contentGenerateFailed', updateAt: moment() }, { where: { id: solution.id } } ); return; } // 更新方案状态为生成成功 await models.Solutions.update( { status: 'contentGenerateSuccess', updateAt: moment() }, { where: { id: solution.id } } ); if (usageTrackQueue.length > 0) { setImmediate(() => { Promise.allSettled( usageTrackQueue.map((payload) => trackGenerateUsage(ctx, payload)) ).catch((usageError) => { ctx?.logger?.log?.(usageError); }); }); } await reportBusinessCall({ ctx, applicationId: 'stable-industry', eventId: `solution:${solution.id}:content:${Date.now()}`, traceId: `solution:${solution.id}:content`, userId: solution.creator, }); console.log('所有章节内容生成完成'); } catch (error) { console.log('generateAllContent error:', error) await models.Solutions.update( { status: 'contentGenerateFailed', updateAt: moment() }, { where: { id: solution.id } } ); } }; module.exports.reWriteChapterContent = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); const { models, ORM: { Op } } = ctx.app.fs.dc; try { const { apiUrl: fastGptApiUrl, solutionAppKey } = ctx.app.fs.config.fastGpt; const { solutionId, chapterId } = ctx.params; const { targetSections, targetSegments, targetSegmentsWords, writingDirection, confirmCharge, confirmToken, } = ctx.request.body; if (!solutionId || !chapterId) { throw '缺少参数' }; const solution = await models.Solutions.findOne({ where: { id: solutionId }, raw: true, }); const registerSource = await resolveCurrentRegisterSource(ctx, solution?.creator); const quotaService = getQuotaService(ctx); const userId = quotaService?.normalizeUserId?.(solution?.creator); let rewriteOverLimit = false; if (userId) { const quotaTransaction = await ctx.app.fs.dc.orm.transaction(); try { const rewriteQuotaCheck = await quotaService.checkAndRecordRewrite({ transaction: quotaTransaction, userId, docId: Number(solutionId), docKind: quotaService.DOC_KIND.PLAN, chapterKey: `chapter:${chapterId}`, action: 'rewrite', confirmCharge, confirmToken, externalUserId: ctx?.fs?.userIdMapping?.externalUserId || '', }); if (rewriteQuotaCheck?.passed === false) { await quotaTransaction.rollback(); await transaction.rollback(); respondQuotaLimited(ctx, { message: rewriteQuotaCheck.message, limitType: 'rewrite', docKind: 'plan', code: rewriteQuotaCheck.code, confirmToken: rewriteQuotaCheck.confirmToken || null, confirmExpireAt: rewriteQuotaCheck.confirmExpireAt || null, }); return; } rewriteOverLimit = rewriteQuotaCheck?.overLimit === true; await quotaTransaction.commit(); } catch (quotaError) { await quotaTransaction.rollback(); throw quotaError; } } await models.SolutionChapters.update( { targetSections, targetSegments, targetSegmentsWords, writingDirection }, { where: { id: chapterId, solutionId } } ); const chapter = await models.SolutionChapters.findOne({ where: { id: chapterId, solutionId }, raw: true, }); const usageTrackQueue = []; const enqueueGenerateUsage = (payload) => { usageTrackQueue.push(payload); }; if ( chapter.level === 2 || (chapter.level === 1 && !targetSegments) || (chapter.level === 0 && !targetSections) ) { const variables = { generation_type: '内容生成', registerSource, }; if (solution.projectInfo) { variables.projectInfo = solution.projectInfo.map(item => item?.title + ':' + item?.content).join('\n'); } if (solution.requirements) { variables.globalRequirements = solution.requirements; } if (chapter.requirements) { variables.chapterRequirements = chapter.requirements; } if (chapter.writingDirection) { variables.chapterWritingDirection = chapter.writingDirection; } const secondChapter = await models.SolutionChapters.findOne({ attributes: ['id', 'title', 'parentId'], where: { id: chapter.parentId, solutionId }, raw: true, }); let topChapter = null; if (secondChapter) { topChapter = await models.SolutionChapters.findOne({ attributes: ['title'], where: { id: secondChapter.parentId, solutionId }, raw: true, }); } let sendText = topChapter && secondChapter ? `编写章节{topChapter.title}-${secondChapter.title}-${chapter.title}` : secondChapter ? `编写章节{secondChapter.title}-${chapter.title}` : `编写章节{chapter.title}`; if (chapter.targetCharts) { sendText += `\n图表数:${chapter.targetCharts}`; } if (chapter.targetTables) { sendText += `\n表格数:${chapter.targetTables}`; } if (chapter.targetSegmentsWords) { sendText += `\n目标字数:约${chapter.targetSegmentsWords}字`; } const res = await superagent.post(`${fastGptApiUrl}/api/v1/chat/completions`) .send(await withRegisterSourceVariables(ctx, { "stream": false, "detail": true, "variables": variables, "messages": [ { "role": "user", "content": [ { "type": "text", "text": sendText }, ] } ] }, solution?.creator)) .set({ Authorization: `Bearer ${solutionAppKey}`, "Content-Type": "application/json", }); const content = res.body.choices[0].message.content; enqueueGenerateUsage({ creator: solution?.creator, docId: solution?.id, chapterKey: `chapter:${chapter.id}`, responseBody: res.body, ignoreCreateFree: rewriteOverLimit, allowChapterFirstFree: !rewriteOverLimit, }); const existingVersion = await models.SolutionChapterVersions.findOne({ where: { solutionId: solution.id, chapterId: chapter.id }, attributes: ['version'], order: [['version', 'DESC']], raw: true }); const newVersion = existingVersion ? existingVersion.version + 1 : 1; if (existingVersion) { await models.SolutionChapterVersions.update( { isCurrent: false }, { where: { chapterId, solutionId, isCurrent: true }, transaction } ); } await models.SolutionChapterVersions.create( { solutionId: solution.id, chapterId: chapter.id, version: newVersion, isCurrent: true, content: content }, { transaction } ); } else if (chapter.level === 1) { // 段 const splitRes = await superagent.post(`${fastGptApiUrl}/api/v1/chat/completions`) .send(await withRegisterSourceVariables(ctx, { "stream": false, "detail": true, "variables": { generation_type: '生成部分目录' }, "messages": [ { "role": "user", "content": `父目录:${chapter.title}\n生成${targetSegments}个二级目录,不要生成三级目录\n生成方向${writingDirection}` } ] }, solution?.creator)) .set({ Authorization: `Bearer ${solutionAppKey}`, "Content-Type": "application/json", }); const splitContent = JSON.parse(splitRes.body.choices[0].message.content); enqueueGenerateUsage({ creator: solution?.creator, docId: solution?.id, responseBody: splitRes.body, ignoreCreateFree: rewriteOverLimit, allowChapterFirstFree: !rewriteOverLimit, }); // 删除旧的子目录和内容 const oldChapters = await models.SolutionChapters.findAll({ attributes: ['id'], where: { parentId: chapterId, solutionId }, raw: true, transaction }); await models.SolutionChapterVersions.destroy({ where: { chapterId: { [Op.in]: oldChapters.map(item => item.id) } }, transaction }); await models.SolutionChapters.destroy({ where: { parentId: Number(chapterId), solutionId }, transaction }); // 创建新的子目录 const newChapters = await models.SolutionChapters.bulkCreate(splitContent.map((item, index) => ({ parentId: Number(chapterId), solutionId: Number(solutionId), title: item.name, level: 2, sortOrder: index, })), { returning: true, transaction }); // 生成内容 let lastContent = ''; const topChapter = await models.SolutionChapters.findOne({ attributes: ['title'], where: { id: chapter.parentId, solutionId }, raw: true, }); for (const item of newChapters) { if (chapter) { const variables = { generation_type: '内容生成', lastContent, registerSource, }; if (solution.projectInfo) { variables.projectInfo = solution.projectInfo.map(item => item?.title + ':' + item?.content).join('\n'); } if (solution.requirements) { variables.globalRequirements = solution.requirements; } if (item.writingDirection) { variables.chapterWritingDirection = item.writingDirection; } let sendText = `编写章节:${topChapter?.title || chapter.title}-${chapter.title}-${item.title}`; if (targetSegmentsWords) { sendText += `\n目标字数:约${targetSegmentsWords}字`; } const res = await superagent.post(`${fastGptApiUrl}/api/v1/chat/completions`) .send(await withRegisterSourceVariables(ctx, { "stream": false, "detail": true, "variables": variables, "messages": [ { "role": "user", "content": [ { "type": "text", "text": sendText }, ] } ] }, solution?.creator)) .set({ Authorization: `Bearer ${solutionAppKey}`, "Content-Type": "application/json", }); const content = res.body.choices[0].message.content; lastContent = content; enqueueGenerateUsage({ creator: solution?.creator, docId: solution?.id, chapterKey: `chapter:${item.id}`, responseBody: res.body, ignoreCreateFree: rewriteOverLimit, allowChapterFirstFree: !rewriteOverLimit, }); await models.SolutionChapterVersions.create( { solutionId: solution.id, chapterId: item.id, version: 1, isCurrent: true, content: content }, { transaction } ); } } } else if (chapter.level === 0) { // 章 // 调用fastgpt生成目录结构 const splitRes = await superagent.post(`${fastGptApiUrl}/api/v1/chat/completions`) .send(await withRegisterSourceVariables(ctx, { "stream": false, "detail": true, "variables": { generation_type: '生成部分目录' }, "messages": [ { "role": "user", "content": `父目录:${chapter.title}\n生成${targetSections}个二级目录,每个二级目录生成${targetSegments}个三级目录\n生成方向${writingDirection}` } ] }, solution?.creator)) .set({ Authorization: `Bearer ${solutionAppKey}`, "Content-Type": "application/json", }); const splitContent = JSON.parse(splitRes.body.choices[0].message.content); enqueueGenerateUsage({ creator: solution?.creator, docId: solution?.id, responseBody: splitRes.body, ignoreCreateFree: rewriteOverLimit, allowChapterFirstFree: !rewriteOverLimit, }); // 删除旧的子目录和所有子孙目录的内容 const oldSections = await models.SolutionChapters.findAll({ attributes: ['id'], where: { parentId: Number(chapterId), solutionId: Number(solutionId) }, raw: true, transaction }); if (oldSections.length > 0) { const oldSegments = await models.SolutionChapters.findAll({ attributes: ['id'], where: { parentId: { [Op.in]: oldSections.map(item => item.id) }, solutionId: Number(solutionId) }, raw: true, transaction }); // 删除段的版本记录 if (oldSegments.length > 0) { await models.SolutionChapterVersions.destroy({ where: { chapterId: { [Op.in]: oldSegments.map(item => item.id) } }, transaction }); } // 删除节的版本记录 await models.SolutionChapterVersions.destroy({ where: { chapterId: { [Op.in]: oldSections.map(item => item.id) } }, transaction }); // 删除所有段 if (oldSegments.length > 0) { await models.SolutionChapters.destroy({ where: { id: { [Op.in]: oldSegments.map(item => item.id) } }, transaction }); } // 删除所有节 await models.SolutionChapters.destroy({ where: { parentId: Number(chapterId), solutionId: Number(solutionId) }, transaction }); } let lastContent = ''; // 创建新的节和段 for (let i = 0; i < splitContent.length; i++) { const sectionData = splitContent[i]; // 创建节 const newSection = await models.SolutionChapters.create({ parentId: Number(chapterId), solutionId: Number(solutionId), title: sectionData.name, level: 1, sortOrder: i, }, { transaction }); // 创建段 if (sectionData.child && sectionData.child.length > 0) { const newSegments = await models.SolutionChapters.bulkCreate( sectionData.child.map((segmentData, segmentIndex) => ({ parentId: newSection.id, solutionId: Number(solutionId), title: segmentData.name, level: 2, sortOrder: segmentIndex, })), { returning: true, transaction } ); // 为每个段生成内容 for (const segment of newSegments) { const variables = { generation_type: '内容生成', lastContent, registerSource, }; if (solution.projectInfo) { variables.projectInfo = solution.projectInfo.map(item => item?.title + ':' + item?.content).join('\n'); } if (solution.requirements) { variables.globalRequirements = solution.requirements; } if (segment.writingDirection) { variables.chapterWritingDirection = segment.writingDirection; } let sendText = `编写章节:${chapter.title}-${newSection.title}-${segment.title}`; if (targetSegmentsWords) { sendText += `\n目标字数:约${targetSegmentsWords}字`; } const res = await superagent.post(`${fastGptApiUrl}/api/v1/chat/completions`) .send(await withRegisterSourceVariables(ctx, { "stream": false, "detail": true, "variables": variables, "messages": [ { "role": "user", "content": [ { "type": "text", "text": sendText } ] } ] }, solution?.creator)) .set({ Authorization: `Bearer ${solutionAppKey}`, "Content-Type": "application/json", }); const content = res.body.choices[0].message.content; lastContent = content; enqueueGenerateUsage({ creator: solution?.creator, docId: solution?.id, chapterKey: `chapter:${segment.id}`, responseBody: res.body, ignoreCreateFree: rewriteOverLimit, allowChapterFirstFree: !rewriteOverLimit, }); await models.SolutionChapterVersions.create( { solutionId: solution.id, chapterId: segment.id, version: 1, isCurrent: true, content: content }, { transaction } ); } } } } else { // throw '无效的章节级别'; } await transaction.commit(); if (usageTrackQueue.length > 0) { // 生成请求先返回,token 计费与日志异步落库,避免重编接口串行等待扣费链路。 setImmediate(() => { Promise.allSettled( usageTrackQueue.map((payload) => trackGenerateUsage(ctx, payload)) ).catch((usageError) => { ctx?.logger?.log?.(usageError); }); }); } await reportBusinessCall({ ctx, applicationId: 'stable-industry', eventId: `solution:${solutionId}:rewrite:${Date.now()}`, traceId: `solution:${solutionId}:rewrite:${chapterId}`, userId: solution?.creator, }); ctx.status = 204; } catch (error) { await transaction.rollback(); ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : '重写章节内容失败' }; } }; module.exports.getChapterVersions = async (ctx, next) => { try { const { models } = ctx.app.fs.dc; const { chapterId } = ctx.params; const versions = await models.SolutionChapterVersions.findAll({ where: { chapterId: Number(chapterId) }, order: [['version', 'desc']], raw: true }); ctx.status = 200; ctx.body = versions; } catch (error) { ctx.logger.log(error); ctx.status = 400; ctx.body = { message: '获取章节版本列表失败' }; } }; module.exports.saveChapterEdit = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models } = ctx.app.fs.dc; const { solutionId, chapterId } = ctx.params; const { content } = ctx.request.body; // 旧版本处理 const oldVersion = await models.SolutionChapterVersions.findOne({ where: { chapterId: Number(chapterId), isCurrent: true }, raw: true }); if (oldVersion) { await models.SolutionChapterVersions.update({ isCurrent: false }, { where: { chapterId: Number(chapterId), isCurrent: true }, transaction }); } await models.SolutionChapterVersions.create({ solutionId: Number(solutionId), chapterId: Number(chapterId), version: oldVersion ? oldVersion.version + 1 : 1, isCurrent: true, content: content }, { transaction }); await transaction.commit(); ctx.status = 204; } catch (error) { await transaction.rollback(); ctx.logger.log(error); ctx.status = 400; ctx.body = { message: '保存章节编辑失败' }; } };