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