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