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.
 
 
 

442 lines
15 KiB

'use strict';
const plagiarismCheckService = require('../utils/plagiarismCheckService');
const { recordAiQueryLog } = require('../utils/aiUsage');
const toInt = (value, defaultValue = null) => {
const parsed = Number.parseInt(String(value), 10);
return Number.isFinite(parsed) ? parsed : defaultValue;
};
const getErrorMessage = (error, fallback) =>
typeof error === 'string' ? error : (error?.message || fallback);
const normalizeTaskUserId = (ctx) => {
const body = ctx.request.body || {};
return toInt(body.userId || body.externalUserId || ctx.state?.externalUserId || ctx.state?.user?.id);
};
module.exports.createTask = async (ctx) => {
try {
const data = await plagiarismCheckService.createTask(ctx);
try {
await recordAiQueryLog(ctx, {
userId: normalizeTaskUserId(ctx),
ipAddress: String(ctx.request.body?.curIp || '').trim() || null,
feature: '文档查重',
});
} catch (error) {
ctx.logger?.log?.(error);
}
ctx.status = 200;
ctx.body = {
message: '提交成功',
data,
};
} catch (error) {
ctx.logger?.log?.(error);
ctx.status = 400;
ctx.body = { message: getErrorMessage(error, '提交查重任务失败') };
}
};
module.exports.getTaskDetail = async (ctx) => {
try {
const { models } = ctx.app.fs.dc;
const taskId = toInt(ctx.params.taskId);
if (!taskId) throw new Error('缺少参数: taskId');
const task = await models.PlagiarismCheckTasks.findByPk(taskId, {
include: [
{
model: models.PlagiarismCheckFiles,
required: false,
include: [
{
model: models.PlagiarismCheckDocTypes,
required: false,
},
{
model: models.PlagiarismCheckFileSummaries,
required: false,
},
],
},
{
model: models.PlagiarismCheckFileSummaries,
required: false,
},
],
order: [[models.PlagiarismCheckFiles, 'sortOrder', 'ASC']],
});
if (!task) throw new Error('查重任务不存在');
ctx.status = 200;
ctx.body = task;
} catch (error) {
ctx.logger?.log?.(error);
ctx.status = 400;
ctx.body = { message: getErrorMessage(error, '获取查重任务失败') };
}
};
module.exports.getFileBlocks = async (ctx) => {
try {
const { models } = ctx.app.fs.dc;
const taskId = toInt(ctx.params.taskId);
const fileId = toInt(ctx.params.fileId);
if (!taskId) throw new Error('缺少参数: taskId');
if (!fileId) throw new Error('缺少参数: fileId');
const page = Math.max(toInt(ctx.request.query.page, 1), 1);
const pageSize = Math.min(Math.max(toInt(ctx.request.query.pageSize, 200), 1), 1000);
const file = await models.PlagiarismCheckFiles.findOne({
where: { id: fileId, taskId },
raw: true,
});
if (!file) throw new Error('文件不存在或不属于当前任务');
const result = await models.PlagiarismCheckFileBlocks.findAndCountAll({
where: { taskId, fileId },
order: [['blockIndex', 'ASC']],
offset: (page - 1) * pageSize,
limit: pageSize,
});
ctx.status = 200;
ctx.body = {
count: result.count,
rows: result.rows.map(plagiarismCheckService.buildEditableBlockResponse),
page,
pageSize,
};
} catch (error) {
ctx.logger?.log?.(error);
ctx.status = 400;
ctx.body = { message: getErrorMessage(error, '获取解析块失败') };
}
};
module.exports.updateFileBlock = async (ctx) => {
try {
const taskId = toInt(ctx.params.taskId);
const fileId = toInt(ctx.params.fileId);
const blockId = toInt(ctx.params.blockId);
if (!taskId) throw new Error('缺少参数: taskId');
if (!fileId) throw new Error('缺少参数: fileId');
if (!blockId) throw new Error('缺少参数: blockId');
const body = ctx.request.body || {};
const result = await plagiarismCheckService.updateBlockContent(ctx, {
taskId,
fileId,
blockId,
htmlContent: body.htmlContent,
plainText: body.plainText,
editSource: body.editSource,
});
ctx.status = 200;
ctx.body = result;
} catch (error) {
ctx.logger?.log?.(error);
ctx.status = 400;
ctx.body = { message: getErrorMessage(error, '保存解析块失败') };
}
};
module.exports.deduplicateFileBlock = async (ctx) => {
try {
const taskId = toInt(ctx.params.taskId);
const fileId = toInt(ctx.params.fileId);
const blockId = toInt(ctx.params.blockId);
if (!taskId) throw new Error('缺少参数: taskId');
if (!fileId) throw new Error('缺少参数: fileId');
if (!blockId) throw new Error('缺少参数: blockId');
const body = ctx.request.body || {};
const result = await plagiarismCheckService.generateDeduplicatedBlock(ctx, {
taskId,
fileId,
blockId,
htmlContent: body.htmlContent,
plainText: body.plainText,
prompt: body.prompt,
});
ctx.status = 200;
ctx.body = result;
} catch (error) {
ctx.logger?.log?.(error);
ctx.status = 400;
ctx.body = { message: getErrorMessage(error, '块内容降重失败') };
}
};
module.exports.deleteFileBlock = async (ctx) => {
try {
const taskId = toInt(ctx.params.taskId);
const fileId = toInt(ctx.params.fileId);
const blockId = toInt(ctx.params.blockId);
if (!taskId) throw new Error('缺少参数: taskId');
if (!fileId) throw new Error('缺少参数: fileId');
if (!blockId) throw new Error('缺少参数: blockId');
await plagiarismCheckService.deleteFileBlock(ctx, {
taskId,
fileId,
blockId,
});
ctx.status = 204;
} catch (error) {
ctx.logger?.log?.(error);
ctx.status = 400;
ctx.body = { message: getErrorMessage(error, '删除解析块失败') };
}
};
module.exports.exportTaskFileDocx = async (ctx) => {
try {
const taskId = toInt(ctx.params.taskId);
const fileId = toInt(ctx.params.fileId);
if (!taskId) throw new Error('缺少参数: taskId');
if (!fileId) throw new Error('缺少参数: fileId');
const result = await plagiarismCheckService.exportTaskFileDocx(ctx, {
taskId,
fileId,
});
ctx.set('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
ctx.set('Content-Disposition', `attachment; filename="${encodeURIComponent(result.fileName)}"`);
ctx.body = result.buffer;
ctx.status = 200;
} catch (error) {
ctx.logger?.log?.(error);
ctx.status = 400;
ctx.body = { message: getErrorMessage(error, '导出文档失败') };
}
};
module.exports.updateTaskThreshold = async (ctx) => {
try {
const taskId = toInt(ctx.params.taskId);
if (!taskId) throw new Error('缺少参数: taskId');
const body = ctx.request.body || {};
const data = await plagiarismCheckService.updateTaskThreshold(ctx, {
taskId,
threshold: body.threshold,
});
ctx.status = 200;
ctx.body = {
message: '阈值更新成功',
data,
};
} catch (error) {
ctx.logger?.log?.(error);
ctx.status = 400;
ctx.body = { message: getErrorMessage(error, '更新查重阈值失败') };
}
};
module.exports.getTaskMatches = async (ctx) => {
try {
const { models, ORM: { Op } } = ctx.app.fs.dc;
const taskId = toInt(ctx.params.taskId);
if (!taskId) throw new Error('缺少参数: taskId');
const page = Math.max(toInt(ctx.request.query.page, 1), 1);
const pageSize = Math.min(Math.max(toInt(ctx.request.query.pageSize, 200), 1), 1000);
const where = { taskId };
const fileId = toInt(ctx.request.query.fileId);
const leftFileId = toInt(ctx.request.query.leftFileId);
const rightFileId = toInt(ctx.request.query.rightFileId);
const matchType = String(ctx.request.query.matchType || '').trim();
if (fileId) where[Op.or] = [{ leftFileId: fileId }, { rightFileId: fileId }];
if (leftFileId) where.leftFileId = leftFileId;
if (rightFileId) where.rightFileId = rightFileId;
if (matchType) {
where.matchType = matchType === 'red'
? 'same_type'
: (matchType === 'yellow' ? 'cross_type' : matchType);
}
const result = await models.PlagiarismCheckMatches.findAndCountAll({
where,
order: [['id', 'ASC']],
offset: (page - 1) * pageSize,
limit: pageSize,
});
ctx.status = 200;
ctx.body = {
count: result.count,
rows: result.rows,
page,
pageSize,
};
} catch (error) {
ctx.logger?.log?.(error);
ctx.status = 400;
ctx.body = { message: getErrorMessage(error, '获取查重结果失败') };
}
};
module.exports.getTaskResult = async (ctx) => {
try {
const { models } = ctx.app.fs.dc;
const taskId = toInt(ctx.params.taskId);
if (!taskId) throw new Error('缺少参数: taskId');
const task = await models.PlagiarismCheckTasks.findByPk(taskId, { raw: true });
if (!task) throw new Error('查重任务不存在');
const files = await models.PlagiarismCheckFiles.findAll({
where: { taskId },
order: [['sortOrder', 'ASC'], ['id', 'ASC']],
raw: true,
});
const summaries = await models.PlagiarismCheckFileSummaries.findAll({
where: { taskId },
order: [['fileId', 'ASC']],
raw: true,
});
const matches = await models.PlagiarismCheckMatches.findAll({
where: { taskId },
order: [['id', 'ASC']],
raw: true,
});
const fileIndexMap = new Map(files.map((file, index) => [Number(file.id), index]));
const mockMatches = matches.map((match, index) => ({
id: `m-${index}`,
dbId: match.id,
leftFileId: match.leftFileId,
rightFileId: match.rightFileId,
leftBlockId: match.leftBlockId,
rightBlockId: match.rightBlockId,
leftIdx: fileIndexMap.get(Number(match.leftFileId)) ?? 0,
rightIdx: fileIndexMap.get(Number(match.rightFileId)) ?? 0,
leftPara: match.leftBlockIndex,
rightPara: match.rightBlockIndex,
len: match.matchLength,
matchType: match.color,
matchText: match.matchText,
leftStart: match.leftStart,
leftEnd: match.leftEnd,
rightStart: match.rightStart,
rightEnd: match.rightEnd,
leftRawStart: match.leftRawStart,
leftRawEnd: match.leftRawEnd,
rightRawStart: match.rightRawStart,
rightRawEnd: match.rightRawEnd,
}));
ctx.status = 200;
ctx.body = {
task,
files,
summaries,
matches: mockMatches,
};
} catch (error) {
ctx.logger?.log?.(error);
ctx.status = 400;
ctx.body = { message: getErrorMessage(error, '获取查重汇总失败') };
}
};
module.exports.rerunTaskCheck = async (ctx) => {
try {
const taskId = toInt(ctx.params.taskId);
if (!taskId) throw new Error('缺少参数: taskId');
const result = await plagiarismCheckService.runPlagiarismCheck(ctx, taskId);
ctx.status = 200;
ctx.body = {
message: '查重完成',
data: result,
};
} catch (error) {
ctx.logger?.log?.(error);
ctx.status = 400;
ctx.body = { message: getErrorMessage(error, '重新查重失败') };
}
};
module.exports.getDocTypes = async (ctx) => {
try {
const { models } = ctx.app.fs.dc;
const includeDisabled = String(ctx.request.query.includeDisabled || '') === 'true';
const where = includeDisabled ? {} : { enabled: true };
const rows = await models.PlagiarismCheckDocTypes.findAll({
where,
order: [['sortOrder', 'ASC'], ['id', 'ASC']],
});
ctx.status = 200;
ctx.body = rows;
} catch (error) {
ctx.logger?.log?.(error);
ctx.status = 400;
ctx.body = { message: getErrorMessage(error, '获取文档类型失败') };
}
};
module.exports.createDocType = async (ctx) => {
try {
const { models } = ctx.app.fs.dc;
const body = ctx.request.body || {};
const typeCode = String(body.typeCode || '').trim();
const typeName = String(body.typeName || '').trim();
if (!typeCode) throw new Error('缺少参数: typeCode');
if (!typeName) throw new Error('缺少参数: typeName');
const created = await models.PlagiarismCheckDocTypes.create({
typeCode,
typeName,
sortOrder: toInt(body.sortOrder, 0),
enabled: body.enabled === undefined ? true : Boolean(body.enabled),
updatedAt: new Date(),
}, { returning: true });
ctx.status = 200;
ctx.body = created;
} catch (error) {
ctx.logger?.log?.(error);
ctx.status = 400;
ctx.body = { message: getErrorMessage(error, '创建文档类型失败') };
}
};
module.exports.updateDocType = async (ctx) => {
try {
const { models } = ctx.app.fs.dc;
const docTypeId = toInt(ctx.params.docTypeId);
if (!docTypeId) throw new Error('缺少参数: docTypeId');
const body = ctx.request.body || {};
const payload = { updatedAt: new Date() };
if (body.typeCode !== undefined) payload.typeCode = String(body.typeCode || '').trim();
if (body.typeName !== undefined) payload.typeName = String(body.typeName || '').trim();
if (body.sortOrder !== undefined) payload.sortOrder = toInt(body.sortOrder, 0);
if (body.enabled !== undefined) payload.enabled = Boolean(body.enabled);
if (payload.typeCode === '') throw new Error('typeCode 不能为空');
if (payload.typeName === '') throw new Error('typeName 不能为空');
const [count] = await models.PlagiarismCheckDocTypes.update(payload, {
where: { id: docTypeId },
});
if (!count) throw new Error('文档类型不存在');
ctx.status = 204;
} catch (error) {
ctx.logger?.log?.(error);
ctx.status = 400;
ctx.body = { message: getErrorMessage(error, '更新文档类型失败') };
}
};
module.exports.deleteDocType = async (ctx) => {
try {
const { models } = ctx.app.fs.dc;
const docTypeId = toInt(ctx.params.docTypeId);
if (!docTypeId) throw new Error('缺少参数: docTypeId');
const usingCount = await models.PlagiarismCheckFiles.count({
where: { docTypeId },
});
if (usingCount > 0) {
await models.PlagiarismCheckDocTypes.update({
enabled: false,
updatedAt: new Date(),
}, { where: { id: docTypeId } });
ctx.status = 204;
return;
}
await models.PlagiarismCheckDocTypes.destroy({ where: { id: docTypeId } });
ctx.status = 204;
} catch (error) {
ctx.logger?.log?.(error);
ctx.status = 400;
ctx.body = { message: getErrorMessage(error, '删除文档类型失败') };
}
};