ai-query对接新版freesun-agent接口的分支
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1677 lines
57 KiB

'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: '保存章节编辑失败'
};
}
};