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.
1766 lines
56 KiB
1766 lines
56 KiB
'use strict';
|
|
|
|
const superagent = require('superagent');
|
|
const moment = require('moment');
|
|
const { reportBusinessCall, reportFastgptResponse } = require('../services/dashboardReporter');
|
|
|
|
const PATENT_STATUS_ENUM = {
|
|
'created': '刚创建',
|
|
'chaptersGenerating': '预览目录生成中',
|
|
'chaptersGenerateSuccess': '预览目录生成成功',
|
|
'chaptersGenerateFailed': '预览目录生成失败',
|
|
'chaptersCreated': '已确认创建目录',
|
|
'contentGenerating': '内容生成中',
|
|
'contentGenerateSuccess': '内容生成成功',
|
|
'contentGenerateFailed': '内容生成失败',
|
|
};
|
|
|
|
const DEFAULT_BASE_FIELDS = [
|
|
{ fieldKey: 'invention_content', fieldTitle: '发明内容', sortOrder: 0 },
|
|
{ fieldKey: 'background_technology', fieldTitle: '背景技术', sortOrder: 1 },
|
|
{ fieldKey: 'closest_tech_problems', fieldTitle: '最接近技术存在问题及技术原因', sortOrder: 2 },
|
|
{ fieldKey: 'technical_problem_to_solve', fieldTitle: '本申请要解决的技术问题', sortOrder: 3 },
|
|
{ fieldKey: 'key_points_and_protection', fieldTitle: '关键点和保护点', sortOrder: 4 },
|
|
{ fieldKey: 'full_technical_solution', fieldTitle: '完整技术方案', sortOrder: 5 },
|
|
{ fieldKey: 'alternative_solutions', fieldTitle: '替代方案', sortOrder: 6 },
|
|
{ fieldKey: 'specific_implementation', fieldTitle: '具体实施方式', sortOrder: 7 },
|
|
{ fieldKey: 'beneficial_effects', fieldTitle: '有益效果', sortOrder: 8 },
|
|
{ fieldKey: 'drawings_and_notes', fieldTitle: '附图及说明', sortOrder: 9 },
|
|
{ fieldKey: 'other_materials', fieldTitle: '其他有助于理解资料', sortOrder: 10 },
|
|
];
|
|
|
|
const PATENT_DISCLOSURE_PREFACE_HTML = `
|
|
<table style="width:100%;border-collapse:collapse;">
|
|
<tbody>
|
|
<tr>
|
|
<td style="border:1px solid #000;">专利名称</td>
|
|
<td colspan="5" style="border:1px solid #000;"></td>
|
|
</tr>
|
|
<tr>
|
|
<td style="border:1px solid #000;">技术联系人</td>
|
|
<td style="border:1px solid #000;"></td>
|
|
<td style="border:1px solid #000;">电话</td>
|
|
<td style="border:1px solid #000;"></td>
|
|
<td style="border:1px solid #000;">申请类型(发明、实用、外观)</td>
|
|
<td style="border:1px solid #000;"></td>
|
|
</tr>
|
|
<tr>
|
|
<td style="border:1px solid #000;">发明人</td>
|
|
<td colspan="3" style="border:1px solid #000;"></td>
|
|
<td style="border:1px solid #000;">本申请所属的技术领域(比如化妆品、汽车、电子通信、计算机)</td>
|
|
<td style="border:1px solid #000;"></td>
|
|
</tr>
|
|
<tr>
|
|
<td style="border:1px solid #000;">专利权人</td>
|
|
<td style="border:1px solid #000;"></td>
|
|
<td style="border:1px solid #000;">研发立项项目</td>
|
|
<td style="border:1px solid #000;"></td>
|
|
<td style="border:1px solid #000;">关联产品</td>
|
|
<td style="border:1px solid #000;"></td>
|
|
</tr>
|
|
<tr>
|
|
<td style="border:1px solid #000;">申请专利的目的</td>
|
|
<td colspan="5" style="border:1px solid #000;">□保护, □干扰竞争对手, □对抗/进攻对手, □其他___________</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
`.trim();
|
|
|
|
const PATENT_INFO_CHAPTER_TITLE = '专利信息';
|
|
const normalizeChapterTitle = (value = '') =>
|
|
String(value || '').trim().replace(/\s+/g, '');
|
|
const isPatentInfoChapter = (chapter = {}) =>
|
|
normalizeChapterTitle(chapter?.title || chapter?.name) ===
|
|
normalizeChapterTitle(PATENT_INFO_CHAPTER_TITLE);
|
|
|
|
const getFastGptClient = (ctx) => {
|
|
const { apiUrl, knowledgeAppKey } = ctx.app.fs.config.fastGpt;
|
|
if (!apiUrl || !knowledgeAppKey) {
|
|
throw 'FASTGPT 配置缺失,请检查 FASTGPT_URL 与 FASTGPT_KNOWLEDGE_APP_KEY';
|
|
}
|
|
return { apiUrl, appKey: knowledgeAppKey };
|
|
};
|
|
|
|
const callFastGpt = async (ctx, {
|
|
variables = {},
|
|
messages = [],
|
|
userId = '',
|
|
actionId = '',
|
|
includeCall = true,
|
|
reportContext = {},
|
|
}) => {
|
|
const { apiUrl, appKey } = getFastGptClient(ctx);
|
|
|
|
const res = await superagent
|
|
.post(`${apiUrl}/api/v1/chat/completions`)
|
|
.send({
|
|
stream: false,
|
|
detail: true,
|
|
variables,
|
|
messages,
|
|
})
|
|
.set({
|
|
Authorization: `Bearer ${appKey}`,
|
|
'Content-Type': 'application/json',
|
|
});
|
|
|
|
await reportFastgptResponse({
|
|
ctx,
|
|
applicationId: 'stable-patent',
|
|
actionId: actionId || `patent:${res?.body?.responseData?.id || res?.body?.dataId || Date.now()}`,
|
|
responseBody: res?.body,
|
|
appKey,
|
|
userId,
|
|
includeCall,
|
|
reportContext,
|
|
});
|
|
return res?.body?.choices?.[0]?.message?.content || '';
|
|
};
|
|
|
|
const parseJsonMaybe = (text) => {
|
|
if (!text) return null;
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch (e) {
|
|
const codeBlock = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
|
|
if (codeBlock?.[1]) {
|
|
try {
|
|
return JSON.parse(codeBlock[1]);
|
|
} catch (err) {
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const toProjectInfoText = (projectInfo) => {
|
|
if (!projectInfo) return '';
|
|
if (typeof projectInfo === 'string') return projectInfo;
|
|
if (!Array.isArray(projectInfo)) return '';
|
|
return projectInfo
|
|
.map(item => `${item?.title || item?.fieldTitle || ''}:${item?.content || item?.fieldValue || ''}`)
|
|
.filter(Boolean)
|
|
.join('\n');
|
|
};
|
|
|
|
const buildChapterMap = (chapters = []) => {
|
|
const chapterMap = new Map();
|
|
chapters.forEach(chapter => chapterMap.set(chapter.id, chapter));
|
|
return chapterMap;
|
|
};
|
|
|
|
const buildChapterPath = (chapter, chapterMap) => {
|
|
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();
|
|
chapters.forEach(chapter => {
|
|
const parentId = chapter.parentId || 'root';
|
|
if (!chaptersByParent.has(parentId)) {
|
|
chaptersByParent.set(parentId, []);
|
|
}
|
|
chaptersByParent.get(parentId).push(chapter);
|
|
});
|
|
|
|
chaptersByParent.forEach(siblings => {
|
|
siblings.sort((a, b) => {
|
|
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder;
|
|
return a.id - b.id;
|
|
});
|
|
});
|
|
|
|
const lines = [];
|
|
const walk = (parentId, prefix = '') => {
|
|
const children = chaptersByParent.get(parentId) || [];
|
|
children.forEach((chapter, index) => {
|
|
const num = prefix ? `${prefix}.${index + 1}` : `${index + 1}`;
|
|
lines.push(`${num} ${chapter.title}`);
|
|
walk(chapter.id, num);
|
|
});
|
|
};
|
|
|
|
walk('root');
|
|
return lines.join('\n');
|
|
};
|
|
|
|
const getOrderedLeafChapters = (chapters = []) => {
|
|
const chapterIdsAsParent = new Set(chapters.filter(item => item.parentId).map(item => item.parentId));
|
|
|
|
const chaptersByParent = new Map();
|
|
chapters.forEach(chapter => {
|
|
const parentId = chapter.parentId || 'root';
|
|
if (!chaptersByParent.has(parentId)) {
|
|
chaptersByParent.set(parentId, []);
|
|
}
|
|
chaptersByParent.get(parentId).push(chapter);
|
|
});
|
|
|
|
chaptersByParent.forEach(siblings => {
|
|
siblings.sort((a, b) => {
|
|
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder;
|
|
return a.id - b.id;
|
|
});
|
|
});
|
|
|
|
const leaf = [];
|
|
const walk = (parentId) => {
|
|
const children = chaptersByParent.get(parentId) || [];
|
|
children.forEach(chapter => {
|
|
if (chapterIdsAsParent.has(chapter.id)) {
|
|
walk(chapter.id);
|
|
} else {
|
|
leaf.push(chapter);
|
|
}
|
|
});
|
|
};
|
|
|
|
walk('root');
|
|
return leaf;
|
|
};
|
|
|
|
const parseAnswerFields = (content = '') => {
|
|
const result = [];
|
|
const regex = /([^\n]+?)----([\s\S]*?)(?=\n[^\n]+?----|$)/g;
|
|
let match = regex.exec(content);
|
|
while (match) {
|
|
const fieldTitle = String(match[1] || '').trim();
|
|
const fieldValue = String(match[2] || '').trim();
|
|
if (fieldTitle) {
|
|
result.push({ fieldTitle, fieldValue });
|
|
}
|
|
match = regex.exec(content);
|
|
}
|
|
return result;
|
|
};
|
|
|
|
const saveChapterVersion = async (models, {
|
|
solutionId,
|
|
chapterId,
|
|
content,
|
|
triggerType = 'generate',
|
|
fromVersion = null,
|
|
promptSnapshot = null,
|
|
transaction = null,
|
|
}) => {
|
|
const whereOptions = {
|
|
where: { chapterId: Number(chapterId), solutionId: Number(solutionId) },
|
|
attributes: ['version'],
|
|
order: [['version', 'DESC']],
|
|
raw: true,
|
|
};
|
|
if (transaction) whereOptions.transaction = transaction;
|
|
|
|
const existingVersion = await models.PatentSolutionChapterVersions.findOne(whereOptions);
|
|
const newVersion = existingVersion ? existingVersion.version + 1 : 1;
|
|
|
|
const updateCurrentOptions = {
|
|
where: { chapterId: Number(chapterId), solutionId: Number(solutionId), isCurrent: true },
|
|
};
|
|
if (transaction) updateCurrentOptions.transaction = transaction;
|
|
|
|
await models.PatentSolutionChapterVersions.update({ isCurrent: false }, updateCurrentOptions);
|
|
|
|
const createData = {
|
|
solutionId: Number(solutionId),
|
|
chapterId: Number(chapterId),
|
|
version: newVersion,
|
|
isCurrent: true,
|
|
triggerType,
|
|
fromVersion,
|
|
content,
|
|
promptSnapshot,
|
|
};
|
|
|
|
const createOptions = {};
|
|
if (transaction) createOptions.transaction = transaction;
|
|
|
|
return await models.PatentSolutionChapterVersions.create(createData, createOptions);
|
|
};
|
|
|
|
const composeDocByCurrentChapters = async (models, solutionId) => {
|
|
const chapters = await models.PatentSolutionChapters.findAll({
|
|
where: { solutionId: Number(solutionId) },
|
|
order: [['sortOrder', 'ASC'], ['id', 'ASC']],
|
|
raw: true,
|
|
});
|
|
|
|
if (!chapters.length) {
|
|
return {
|
|
content: '',
|
|
chapterSnapshot: [],
|
|
};
|
|
}
|
|
|
|
const versions = await models.PatentSolutionChapterVersions.findAll({
|
|
where: { solutionId: Number(solutionId), isCurrent: true },
|
|
raw: true,
|
|
});
|
|
|
|
const versionMap = new Map();
|
|
versions.forEach(item => {
|
|
versionMap.set(item.chapterId, item.content || '');
|
|
});
|
|
|
|
const chapterMap = buildChapterMap(chapters);
|
|
const leafChapters = getOrderedLeafChapters(chapters);
|
|
|
|
const blocks = leafChapters.map((chapter) => {
|
|
const title = buildChapterPath(chapter, chapterMap);
|
|
const content = versionMap.get(chapter.id) || '';
|
|
return `${title}\n${content}`.trim();
|
|
}).filter(Boolean);
|
|
|
|
return {
|
|
content: blocks.join('\n\n'),
|
|
chapterSnapshot: chapters,
|
|
};
|
|
};
|
|
|
|
const saveDocVersion = async (models, {
|
|
solutionId,
|
|
content,
|
|
chapterSnapshot = null,
|
|
triggerType = 'generate',
|
|
fromVersion = null,
|
|
promptSnapshot = null,
|
|
transaction = null,
|
|
}) => {
|
|
const findOptions = {
|
|
where: { solutionId: Number(solutionId) },
|
|
attributes: ['version'],
|
|
order: [['version', 'DESC']],
|
|
raw: true,
|
|
};
|
|
if (transaction) findOptions.transaction = transaction;
|
|
|
|
const latest = await models.PatentSolutionDocVersions.findOne(findOptions);
|
|
const newVersion = latest ? latest.version + 1 : 1;
|
|
|
|
const updateDocOptions = { where: { solutionId: Number(solutionId), isCurrent: true } };
|
|
const updateSolutionOptions = { where: { id: Number(solutionId) } };
|
|
if (transaction) {
|
|
updateDocOptions.transaction = transaction;
|
|
updateSolutionOptions.transaction = transaction;
|
|
}
|
|
|
|
await models.PatentSolutionDocVersions.update({ isCurrent: false }, updateDocOptions);
|
|
|
|
const createOptions = {};
|
|
if (transaction) createOptions.transaction = transaction;
|
|
|
|
const row = await models.PatentSolutionDocVersions.create({
|
|
solutionId: Number(solutionId),
|
|
version: newVersion,
|
|
isCurrent: true,
|
|
triggerType,
|
|
fromVersion,
|
|
content,
|
|
chapterSnapshot,
|
|
promptSnapshot,
|
|
}, createOptions);
|
|
|
|
await models.PatentSolutions.update(
|
|
{ currentDocVersion: newVersion, updateAt: moment() },
|
|
updateSolutionOptions
|
|
);
|
|
|
|
return row;
|
|
};
|
|
|
|
const ensureDefaultBaseFields = async (models, solutionId, transaction = null) => {
|
|
const findOptions = {
|
|
where: { solutionId: Number(solutionId) },
|
|
raw: true,
|
|
order: [['sortOrder', 'ASC'], ['id', 'ASC']],
|
|
};
|
|
if (transaction) findOptions.transaction = transaction;
|
|
|
|
const exists = await models.PatentSolutionBaseFields.findAll(findOptions);
|
|
if (exists.length) return exists;
|
|
|
|
const rows = DEFAULT_BASE_FIELDS.map((item, idx) => ({
|
|
solutionId: Number(solutionId),
|
|
fieldKey: item.fieldKey,
|
|
fieldTitle: item.fieldTitle,
|
|
sortOrder: item.sortOrder,
|
|
isSelected: idx < 5,
|
|
sourceType: 'manual',
|
|
}));
|
|
|
|
const createOptions = {};
|
|
if (transaction) createOptions.transaction = transaction;
|
|
|
|
await models.PatentSolutionBaseFields.bulkCreate(rows, createOptions);
|
|
|
|
return await models.PatentSolutionBaseFields.findAll(findOptions);
|
|
};
|
|
|
|
const generateAllContent = async (ctx, solutionId) => {
|
|
const { models } = ctx.app.fs.dc;
|
|
|
|
try {
|
|
const solution = await models.PatentSolutions.findOne({
|
|
where: { id: Number(solutionId) },
|
|
raw: true,
|
|
});
|
|
if (!solution) {
|
|
return;
|
|
}
|
|
|
|
const allChapters = await models.PatentSolutionChapters.findAll({
|
|
where: { solutionId: Number(solutionId) },
|
|
order: [['sortOrder', 'ASC'], ['id', 'ASC']],
|
|
raw: true,
|
|
});
|
|
|
|
await models.PatentSolutions.update(
|
|
{ status: 'contentGenerating', updateAt: moment() },
|
|
{ where: { id: Number(solutionId) } }
|
|
);
|
|
|
|
const leafChapters = getOrderedLeafChapters(allChapters);
|
|
const chapterMap = buildChapterMap(allChapters);
|
|
const chaptersText = buildChapterStructure(allChapters);
|
|
const projectInfoText = toProjectInfoText(solution.projectInfo);
|
|
let lastContent = '';
|
|
|
|
for (let chapterIndex = 0; chapterIndex < leafChapters.length; chapterIndex += 1) {
|
|
const chapter = leafChapters[chapterIndex];
|
|
if (isPatentInfoChapter(chapter)) {
|
|
await saveChapterVersion(models, {
|
|
solutionId,
|
|
chapterId: chapter.id,
|
|
content: PATENT_DISCLOSURE_PREFACE_HTML,
|
|
triggerType: 'generate',
|
|
promptSnapshot: { source: 'system:patent-info-template' },
|
|
});
|
|
continue;
|
|
}
|
|
const variables = {
|
|
generation_type: '内容生成',
|
|
chapters: chaptersText,
|
|
lastContent,
|
|
};
|
|
|
|
if (projectInfoText) {
|
|
variables.projectInfo = projectInfoText;
|
|
}
|
|
if (solution.globalRequirements) {
|
|
variables.globalRequirements = solution.globalRequirements;
|
|
}
|
|
if (chapter.requirements) {
|
|
variables.chapterRequirements = chapter.requirements;
|
|
}
|
|
if (chapter.writingDirection) {
|
|
variables.chapterWritingDirection = chapter.writingDirection;
|
|
}
|
|
|
|
let sendText = `编写章节:${buildChapterPath(chapter, chapterMap)}`;
|
|
if (chapter.targetWords) {
|
|
sendText += `\n目标字数:约${chapter.targetWords}字`;
|
|
}
|
|
if (chapter.targetCharts) {
|
|
sendText += `\n图表数:${chapter.targetCharts}`;
|
|
}
|
|
if (chapter.targetTables) {
|
|
sendText += `\n表格数:${chapter.targetTables}`;
|
|
}
|
|
|
|
const generatedContent = await callFastGpt(ctx, {
|
|
variables,
|
|
messages: [{ role: 'user', content: [{ type: 'text', text: sendText }] }],
|
|
userId: solution.creator,
|
|
actionId: `patent:${solutionId}:content:chapter:${chapter.id}:${Date.now()}`,
|
|
includeCall: false,
|
|
reportContext: {
|
|
action: 'generateContent',
|
|
solutionId,
|
|
chapterId: chapter.id,
|
|
},
|
|
});
|
|
|
|
await saveChapterVersion(models, {
|
|
solutionId,
|
|
chapterId: chapter.id,
|
|
content: generatedContent,
|
|
triggerType: 'generate',
|
|
promptSnapshot: { variables, sendText },
|
|
});
|
|
|
|
lastContent = generatedContent;
|
|
}
|
|
|
|
const docData = await composeDocByCurrentChapters(models, solutionId);
|
|
await saveDocVersion(models, {
|
|
solutionId,
|
|
content: docData.content,
|
|
chapterSnapshot: docData.chapterSnapshot,
|
|
triggerType: 'generate',
|
|
});
|
|
|
|
await models.PatentSolutions.update(
|
|
{ status: 'contentGenerateSuccess', updateAt: moment() },
|
|
{ where: { id: Number(solutionId) } }
|
|
);
|
|
await reportBusinessCall({
|
|
ctx,
|
|
applicationId: 'stable-patent',
|
|
eventId: `patent:${solutionId}:content:${Date.now()}`,
|
|
traceId: `patent:${solutionId}:content`,
|
|
userId: solution.creator,
|
|
reportContext: { action: 'generateContent', solutionId },
|
|
});
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
await models.PatentSolutions.update(
|
|
{ status: 'contentGenerateFailed', updateAt: moment() },
|
|
{ where: { id: Number(solutionId) } }
|
|
);
|
|
}
|
|
};
|
|
|
|
module.exports.getPatentSolutionList = 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.PatentSolutions.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.createPatentSolution = async (ctx, next) => {
|
|
const transaction = await ctx.app.fs.dc.orm.transaction();
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const {
|
|
name,
|
|
patentType,
|
|
industry,
|
|
projectInfo,
|
|
globalRequirements,
|
|
messages,
|
|
creator,
|
|
curIp,
|
|
baseFields,
|
|
} = ctx.request.body;
|
|
|
|
if (!name) { throw '缺少参数: name'; }
|
|
|
|
const solution = await models.PatentSolutions.create({
|
|
name,
|
|
patentType,
|
|
industry,
|
|
projectInfo,
|
|
globalRequirements,
|
|
messages,
|
|
creator,
|
|
status: 'created',
|
|
createAt: moment(),
|
|
updateAt: moment(),
|
|
}, {
|
|
returning: true,
|
|
transaction,
|
|
});
|
|
|
|
if (Array.isArray(baseFields) && baseFields.length) {
|
|
await models.PatentSolutionBaseFields.bulkCreate(
|
|
baseFields.map((item, index) => ({
|
|
solutionId: solution.id,
|
|
fieldKey: item.fieldKey || `field_${index + 1}`,
|
|
fieldTitle: item.fieldTitle || `字段${index + 1}`,
|
|
fieldDesc: item.fieldDesc || null,
|
|
fieldValue: item.fieldValue || null,
|
|
sortOrder: item.sortOrder ?? index,
|
|
isSelected: item.isSelected ?? index < 5,
|
|
isRequired: item.isRequired ?? false,
|
|
sourceType: item.sourceType || 'manual',
|
|
})),
|
|
{ transaction }
|
|
);
|
|
} else {
|
|
await ensureDefaultBaseFields(models, solution.id, transaction);
|
|
}
|
|
|
|
if (creator || curIp) {
|
|
await models.AiQueryRecord.create({
|
|
userId: creator,
|
|
ipAddress: curIp,
|
|
clientId: 'pep-feixiaoshang',
|
|
feature: '专利方案生成',
|
|
time: moment(),
|
|
}, { 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.modifyPatentSolution = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { solutionId } = ctx.params;
|
|
const body = ctx.request.body;
|
|
|
|
if (!solutionId) { throw '缺少参数: solutionId'; }
|
|
|
|
await models.PatentSolutions.update(
|
|
{ ...body, updateAt: moment() },
|
|
{ where: { id: Number(solutionId) } }
|
|
);
|
|
|
|
ctx.status = 204;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error === 'string' ? error : '修改专利方案失败'
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports.delPatentSolution = async (ctx, next) => {
|
|
const transaction = await ctx.app.fs.dc.orm.transaction();
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { solutionId } = ctx.params;
|
|
|
|
if (!solutionId) { throw '缺少参数: solutionId'; }
|
|
|
|
await models.PatentSolutions.destroy({
|
|
where: { id: Number(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.getBaseFields = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { solutionId } = ctx.params;
|
|
if (!solutionId) { throw '缺少参数: solutionId'; }
|
|
|
|
const rows = await ensureDefaultBaseFields(models, solutionId);
|
|
ctx.body = rows;
|
|
ctx.status = 200;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error === 'string' ? error : '获取基础信息字段失败'
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports.saveBaseFields = async (ctx, next) => {
|
|
const transaction = await ctx.app.fs.dc.orm.transaction();
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { solutionId } = ctx.params;
|
|
const { fields } = ctx.request.body;
|
|
|
|
if (!solutionId || !Array.isArray(fields)) { throw '参数错误'; }
|
|
|
|
await models.PatentSolutionBaseFields.destroy({
|
|
where: { solutionId: Number(solutionId) },
|
|
transaction,
|
|
});
|
|
|
|
if (fields.length) {
|
|
await models.PatentSolutionBaseFields.bulkCreate(fields.map((item, index) => ({
|
|
solutionId: Number(solutionId),
|
|
fieldKey: item.fieldKey || `field_${index + 1}`,
|
|
fieldTitle: item.fieldTitle || `字段${index + 1}`,
|
|
fieldDesc: item.fieldDesc || null,
|
|
fieldValue: item.fieldValue || null,
|
|
sortOrder: item.sortOrder ?? index,
|
|
isSelected: item.isSelected ?? index < 5,
|
|
isRequired: item.isRequired ?? false,
|
|
sourceType: item.sourceType || 'manual',
|
|
sourceChatId: item.sourceChatId || null,
|
|
createAt: moment(),
|
|
updateAt: moment(),
|
|
})), { transaction });
|
|
}
|
|
|
|
await models.PatentSolutions.update(
|
|
{ updateAt: moment() },
|
|
{ where: { id: Number(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.applyAiFields = async (ctx, next) => {
|
|
const transaction = await ctx.app.fs.dc.orm.transaction();
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { solutionId } = ctx.params;
|
|
const { parsedFields = [], sourceChatId } = ctx.request.body;
|
|
|
|
if (!solutionId || !Array.isArray(parsedFields)) { throw '参数错误'; }
|
|
|
|
const fields = await models.PatentSolutionBaseFields.findAll({
|
|
where: { solutionId: Number(solutionId) },
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
|
|
if (!fields.length) {
|
|
await ensureDefaultBaseFields(models, solutionId, transaction);
|
|
}
|
|
|
|
const latestFields = await models.PatentSolutionBaseFields.findAll({
|
|
where: { solutionId: Number(solutionId) },
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
|
|
for (const item of parsedFields) {
|
|
const target = latestFields.find(field =>
|
|
(item.fieldKey && field.fieldKey === item.fieldKey) ||
|
|
(item.fieldTitle && field.fieldTitle === item.fieldTitle)
|
|
);
|
|
|
|
if (target) {
|
|
await models.PatentSolutionBaseFields.update({
|
|
fieldValue: item.fieldValue || '',
|
|
sourceType: 'ai',
|
|
sourceChatId: sourceChatId || null,
|
|
updateAt: moment(),
|
|
}, {
|
|
where: { id: target.id },
|
|
transaction,
|
|
});
|
|
}
|
|
}
|
|
|
|
const updated = await models.PatentSolutionBaseFields.findAll({
|
|
where: { solutionId: Number(solutionId) },
|
|
order: [['sortOrder', 'ASC'], ['id', 'ASC']],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
|
|
await transaction.commit();
|
|
ctx.body = updated;
|
|
ctx.status = 200;
|
|
} catch (error) {
|
|
await transaction.rollback();
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error === 'string' ? error : '应用 AI 字段失败'
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports.generateChapters = async (ctx, next) => {
|
|
const transaction = await ctx.app.fs.dc.orm.transaction();
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { solutionId } = ctx.params;
|
|
const { projectInfo, globalRequirements } = ctx.request.body;
|
|
|
|
if (!solutionId) { throw '缺少参数: solutionId'; }
|
|
|
|
const solution = await models.PatentSolutions.findOne({
|
|
where: { id: Number(solutionId) },
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
if (!solution) { throw '方案不存在'; }
|
|
|
|
const baseFields = await models.PatentSolutionBaseFields.findAll({
|
|
where: { solutionId: Number(solutionId), isSelected: true },
|
|
order: [['sortOrder', 'ASC'], ['id', 'ASC']],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
|
|
const mergedProjectInfo = projectInfo ?? solution.projectInfo;
|
|
const mergedRequirements = globalRequirements ?? solution.globalRequirements;
|
|
|
|
await models.PatentSolutions.update({
|
|
status: 'chaptersGenerating',
|
|
projectInfo: mergedProjectInfo,
|
|
globalRequirements: mergedRequirements,
|
|
updateAt: moment(),
|
|
}, {
|
|
where: { id: Number(solutionId) },
|
|
transaction,
|
|
});
|
|
|
|
const projectInfoText = toProjectInfoText(mergedProjectInfo);
|
|
const baseFieldText = baseFields
|
|
.map(item => `${item.fieldTitle}:${item.fieldValue || ''}`)
|
|
.join('\n');
|
|
|
|
const sendText = [
|
|
`专利方案名称:${solution.name}`,
|
|
projectInfoText ? `项目信息:\n${projectInfoText}` : '',
|
|
mergedRequirements ? `整体要求:\n${mergedRequirements}` : '',
|
|
baseFieldText ? `基础信息:\n${baseFieldText}` : '',
|
|
].filter(Boolean).join('\n\n');
|
|
|
|
const resContent = await callFastGpt(ctx, {
|
|
variables: { generation_type: '目录生成' },
|
|
messages: [{ role: 'user', content: [{ type: 'text', text: sendText }] }],
|
|
userId: solution.creator,
|
|
actionId: `patent:${solutionId}:chapters:${Date.now()}`,
|
|
reportContext: { action: 'generateChapters', solutionId },
|
|
});
|
|
|
|
const previewChapters = parseJsonMaybe(resContent);
|
|
if (!Array.isArray(previewChapters)) {
|
|
throw '目录生成失败,返回格式不是 JSON 数组';
|
|
}
|
|
|
|
await models.PatentSolutions.update({
|
|
status: 'chaptersGenerateSuccess',
|
|
updateAt: moment(),
|
|
}, {
|
|
where: { id: Number(solutionId) },
|
|
transaction,
|
|
});
|
|
|
|
await transaction.commit();
|
|
ctx.body = previewChapters;
|
|
ctx.status = 200;
|
|
} catch (error) {
|
|
await transaction.rollback();
|
|
const { models } = ctx.app.fs.dc;
|
|
if (ctx.params?.solutionId) {
|
|
await models.PatentSolutions.update(
|
|
{ status: 'chaptersGenerateFailed', updateAt: moment() },
|
|
{ where: { id: Number(ctx.params.solutionId) } }
|
|
);
|
|
}
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error === 'string' ? error : '生成目录失败'
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports.sortChapters = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { solutionId } = ctx.params;
|
|
const previewChapters = ctx.request.body;
|
|
|
|
if (!solutionId || !previewChapters) { throw '参数错误'; }
|
|
const solution = await models.PatentSolutions.findOne({
|
|
attributes: ['id', 'creator'],
|
|
where: { id: Number(solutionId) },
|
|
raw: true,
|
|
});
|
|
|
|
const resContent = await callFastGpt(ctx, {
|
|
variables: { generation_type: '整理预览目录' },
|
|
messages: [{ role: 'user', content: JSON.stringify(previewChapters) }],
|
|
userId: solution?.creator,
|
|
actionId: `patent:${solutionId}:sort:${Date.now()}`,
|
|
reportContext: { action: 'sortChapters', solutionId },
|
|
});
|
|
|
|
const newPreviewChapters = parseJsonMaybe(resContent);
|
|
if (!Array.isArray(newPreviewChapters)) {
|
|
throw '整理目录失败,返回格式不是 JSON 数组';
|
|
}
|
|
|
|
await models.PatentSolutions.update({
|
|
updateAt: moment(),
|
|
}, {
|
|
where: { id: Number(solutionId) },
|
|
});
|
|
|
|
ctx.body = newPreviewChapters;
|
|
ctx.status = 200;
|
|
} 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 } = ctx.app.fs.dc;
|
|
const { solutionId } = ctx.params;
|
|
const { previewChapters } = ctx.request.body;
|
|
|
|
if (!solutionId || !Array.isArray(previewChapters)) { throw '参数错误'; }
|
|
const normalizedPreviewChapters = [
|
|
{ name: PATENT_INFO_CHAPTER_TITLE, child: [] },
|
|
...previewChapters.filter((item) => !isPatentInfoChapter(item)),
|
|
];
|
|
|
|
await models.PatentSolutionChapterVersions.destroy({
|
|
where: { solutionId: Number(solutionId) },
|
|
transaction,
|
|
});
|
|
await models.PatentSolutionChapters.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 inserted = await models.PatentSolutionChapters.create({
|
|
solutionId: Number(solutionId),
|
|
title: chapter.name || chapter.title,
|
|
chapterKey: chapter.chapterKey || null,
|
|
parentId,
|
|
sortOrder: sortOrder + i,
|
|
level,
|
|
}, { transaction });
|
|
|
|
if (Array.isArray(chapter.child) && chapter.child.length) {
|
|
await insertChaptersRecursively(chapter.child, inserted.id, 0, level + 1);
|
|
}
|
|
}
|
|
};
|
|
|
|
await insertChaptersRecursively(normalizedPreviewChapters);
|
|
|
|
await models.PatentSolutions.update({
|
|
status: 'chaptersCreated',
|
|
updateAt: moment(),
|
|
}, {
|
|
where: { id: Number(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.getPatentChapters = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { solutionId } = ctx.params;
|
|
if (!solutionId) { throw '缺少参数: solutionId'; }
|
|
|
|
const chapters = await models.PatentSolutionChapters.findAll({
|
|
where: { solutionId: Number(solutionId) },
|
|
order: [['id', 'ASC']],
|
|
include: [{
|
|
model: models.PatentSolutionChapterVersions,
|
|
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.addPatentChapters = 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.PatentSolutionChapters.findOne({
|
|
where: { solutionId: Number(solutionId), parentId: body.parentId || null },
|
|
attributes: ['sortOrder'],
|
|
order: [['sortOrder', 'DESC']],
|
|
raw: true,
|
|
});
|
|
|
|
body.sortOrder = lastChild ? lastChild.sortOrder + 1 : 0;
|
|
await models.PatentSolutionChapters.create(body);
|
|
|
|
ctx.status = 204;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error === 'string' ? error : '添加目录失败'
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports.modifyPatentChapters = 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.PatentSolutionChapters.findOne({
|
|
where: { id: Number(chapterId), solutionId: Number(solutionId) },
|
|
attributes: ['parentId', 'sortOrder'],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
|
|
if (!targetChapter) { throw '章节未找到'; }
|
|
|
|
const originalParentId = targetChapter.parentId;
|
|
const originalSortOrder = targetChapter.sortOrder;
|
|
const newParentId = body.parentId !== undefined ? body.parentId : originalParentId;
|
|
const newSortOrder = body.sortOrder !== undefined ? body.sortOrder : originalSortOrder;
|
|
|
|
const applySortUpdatesSequentially = async (updates = []) => {
|
|
for (const update of updates) {
|
|
await models.PatentSolutionChapters.update(
|
|
{ sortOrder: update.sortOrder, updateAt: moment() },
|
|
{ where: { id: update.id }, transaction }
|
|
);
|
|
}
|
|
};
|
|
|
|
if (newParentId !== originalParentId) {
|
|
await models.PatentSolutionChapters.update(
|
|
{ parentId: newParentId },
|
|
{ where: { id: Number(chapterId) }, transaction }
|
|
);
|
|
|
|
const originalSiblingChapters = await models.PatentSolutionChapters.findAll({
|
|
attributes: ['id', 'sortOrder'],
|
|
where: {
|
|
solutionId: Number(solutionId),
|
|
parentId: originalParentId,
|
|
},
|
|
order: [['sortOrder', 'ASC']],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
|
|
const originalUpdates = originalSiblingChapters
|
|
.filter(chapter => chapter.id !== Number(chapterId))
|
|
.map((chapter, index) => ({ id: chapter.id, sortOrder: index }));
|
|
|
|
if (originalUpdates.length > 0) {
|
|
await applySortUpdatesSequentially(originalUpdates);
|
|
}
|
|
|
|
const newSiblingChapters = await models.PatentSolutionChapters.findAll({
|
|
attributes: ['id', 'sortOrder'],
|
|
where: {
|
|
solutionId: Number(solutionId),
|
|
parentId: newParentId,
|
|
id: { [Op.ne]: Number(chapterId) }
|
|
},
|
|
order: [['sortOrder', 'ASC']],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
|
|
const movingChapter = { id: Number(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.PatentSolutionChapters.findAll({
|
|
attributes: ['id', 'sortOrder'],
|
|
where: {
|
|
solutionId: Number(solutionId),
|
|
parentId: originalParentId,
|
|
},
|
|
order: [['sortOrder', 'ASC']],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
|
|
const currentChapterIndex = siblingChapters.findIndex(chapter => chapter.id === Number(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.PatentSolutionChapters.update(
|
|
{ ...body, updateAt: moment() },
|
|
{
|
|
where: {
|
|
id: Number(chapterId),
|
|
solutionId: Number(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.delPatentChapters = 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 '缺少参数'; }
|
|
|
|
const getChildIds = async (parentId, allChildIds = []) => {
|
|
const children = await models.PatentSolutionChapters.findAll({
|
|
attributes: ['id'],
|
|
where: { parentId },
|
|
transaction,
|
|
});
|
|
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;
|
|
};
|
|
|
|
const allIdsToDelete = [Number(chapterId)];
|
|
const childIds = await getChildIds(Number(chapterId));
|
|
allIdsToDelete.push(...childIds);
|
|
|
|
await models.PatentSolutionChapterVersions.destroy({
|
|
where: {
|
|
chapterId: { [Op.in]: allIdsToDelete },
|
|
solutionId: Number(solutionId),
|
|
},
|
|
transaction,
|
|
});
|
|
|
|
await models.PatentSolutionChapters.destroy({
|
|
where: {
|
|
id: { [Op.in]: allIdsToDelete },
|
|
solutionId: Number(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) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { solutionId } = ctx.params;
|
|
const { globalRequirements, projectInfo } = ctx.request.body;
|
|
|
|
if (!solutionId) { throw '缺少参数: solutionId'; }
|
|
|
|
if (globalRequirements !== undefined || projectInfo !== undefined) {
|
|
const nextUpdate = { updateAt: moment() };
|
|
if (globalRequirements !== undefined) nextUpdate.globalRequirements = globalRequirements;
|
|
if (projectInfo !== undefined) nextUpdate.projectInfo = projectInfo;
|
|
await models.PatentSolutions.update(
|
|
nextUpdate,
|
|
{ where: { id: Number(solutionId) } }
|
|
);
|
|
}
|
|
|
|
generateAllContent(ctx, solutionId);
|
|
|
|
ctx.status = 204;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error === 'string' ? error : '启动正文生成失败'
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports.reWriteChapterContent = 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 { writingDirection, requirements, targetWords, targetCharts, targetTables } = ctx.request.body;
|
|
|
|
if (!solutionId || !chapterId) { throw '缺少参数'; }
|
|
|
|
await models.PatentSolutionChapters.update({
|
|
writingDirection,
|
|
requirements,
|
|
targetWords,
|
|
targetCharts,
|
|
targetTables,
|
|
updateAt: moment(),
|
|
}, {
|
|
where: { id: Number(chapterId), solutionId: Number(solutionId) },
|
|
transaction,
|
|
});
|
|
|
|
const solution = await models.PatentSolutions.findOne({
|
|
where: { id: Number(solutionId) },
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
const chapter = await models.PatentSolutionChapters.findOne({
|
|
where: { id: Number(chapterId), solutionId: Number(solutionId) },
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
if (!solution || !chapter) { throw '方案或章节不存在'; }
|
|
|
|
const allChapters = await models.PatentSolutionChapters.findAll({
|
|
where: { solutionId: Number(solutionId) },
|
|
order: [['sortOrder', 'ASC'], ['id', 'ASC']],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
|
|
const chapterMap = buildChapterMap(allChapters);
|
|
const projectInfoText = toProjectInfoText(solution.projectInfo);
|
|
const chaptersText = buildChapterStructure(allChapters);
|
|
|
|
const variables = {
|
|
generation_type: '内容生成',
|
|
chapters: chaptersText,
|
|
};
|
|
if (projectInfoText) variables.projectInfo = projectInfoText;
|
|
if (solution.globalRequirements) variables.globalRequirements = solution.globalRequirements;
|
|
if (chapter.requirements) variables.chapterRequirements = chapter.requirements;
|
|
if (chapter.writingDirection) variables.chapterWritingDirection = chapter.writingDirection;
|
|
|
|
let sendText = `编写章节:${buildChapterPath(chapter, chapterMap)}`;
|
|
if (chapter.targetWords) sendText += `\n目标字数:约${chapter.targetWords}字`;
|
|
if (chapter.targetCharts) sendText += `\n图表数:${chapter.targetCharts}`;
|
|
if (chapter.targetTables) sendText += `\n表格数:${chapter.targetTables}`;
|
|
|
|
const content = isPatentInfoChapter(chapter)
|
|
? PATENT_DISCLOSURE_PREFACE_HTML
|
|
: await callFastGpt(ctx, {
|
|
variables,
|
|
messages: [{ role: 'user', content: [{ type: 'text', text: sendText }] }],
|
|
userId: solution.creator,
|
|
actionId: `patent:${solutionId}:rewrite:chapter:${chapterId}:${Date.now()}`,
|
|
reportContext: {
|
|
action: 'rewriteChapter',
|
|
solutionId,
|
|
chapterId,
|
|
},
|
|
});
|
|
|
|
await saveChapterVersion(models, {
|
|
solutionId,
|
|
chapterId,
|
|
content,
|
|
triggerType: 'rewrite',
|
|
promptSnapshot: { variables, sendText },
|
|
transaction,
|
|
});
|
|
|
|
const docData = await composeDocByCurrentChapters(models, solutionId);
|
|
await saveDocVersion(models, {
|
|
solutionId,
|
|
content: docData.content,
|
|
chapterSnapshot: docData.chapterSnapshot,
|
|
triggerType: 'rewrite',
|
|
promptSnapshot: { source: 'chapterRewrite', chapterId: Number(chapterId) },
|
|
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.getChapterVersions = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { chapterId } = ctx.params;
|
|
|
|
const versions = await models.PatentSolutionChapterVersions.findAll({
|
|
where: { chapterId: Number(chapterId) },
|
|
order: [['version', 'DESC']],
|
|
raw: true,
|
|
});
|
|
|
|
ctx.body = versions;
|
|
ctx.status = 200;
|
|
} 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;
|
|
|
|
if (!content) { throw '缺少参数: content'; }
|
|
|
|
await saveChapterVersion(models, {
|
|
solutionId,
|
|
chapterId,
|
|
content,
|
|
triggerType: 'manual_save',
|
|
transaction,
|
|
});
|
|
|
|
const docData = await composeDocByCurrentChapters(models, solutionId);
|
|
await saveDocVersion(models, {
|
|
solutionId,
|
|
content: docData.content,
|
|
chapterSnapshot: docData.chapterSnapshot,
|
|
triggerType: 'manual_save',
|
|
promptSnapshot: { source: 'saveChapterEdit', chapterId: Number(chapterId) },
|
|
transaction,
|
|
});
|
|
|
|
await transaction.commit();
|
|
ctx.status = 204;
|
|
} catch (error) {
|
|
await transaction.rollback();
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = { message: '保存章节编辑失败' };
|
|
}
|
|
};
|
|
|
|
module.exports.restoreChapterVersion = async (ctx, next) => {
|
|
const transaction = await ctx.app.fs.dc.orm.transaction();
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { solutionId, chapterId, version } = ctx.params;
|
|
|
|
const targetVersion = await models.PatentSolutionChapterVersions.findOne({
|
|
where: { chapterId: Number(chapterId), version: Number(version) },
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
if (!targetVersion) { throw '目标版本不存在'; }
|
|
|
|
await saveChapterVersion(models, {
|
|
solutionId,
|
|
chapterId,
|
|
content: targetVersion.content,
|
|
triggerType: 'restore',
|
|
fromVersion: Number(version),
|
|
promptSnapshot: { source: 'restoreChapterVersion' },
|
|
transaction,
|
|
});
|
|
|
|
const docData = await composeDocByCurrentChapters(models, solutionId);
|
|
await saveDocVersion(models, {
|
|
solutionId,
|
|
content: docData.content,
|
|
chapterSnapshot: docData.chapterSnapshot,
|
|
triggerType: 'restore',
|
|
promptSnapshot: { source: 'restoreChapterVersion', chapterId: Number(chapterId), version: Number(version) },
|
|
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.getDocVersions = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { solutionId } = ctx.params;
|
|
if (!solutionId) { throw '缺少参数: solutionId'; }
|
|
|
|
const versions = await models.PatentSolutionDocVersions.findAll({
|
|
where: { solutionId: Number(solutionId) },
|
|
order: [['version', 'DESC']],
|
|
raw: true,
|
|
});
|
|
|
|
ctx.body = versions;
|
|
ctx.status = 200;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = { message: typeof error === 'string' ? error : '获取正文版本失败' };
|
|
}
|
|
};
|
|
|
|
module.exports.saveDocEdit = async (ctx, next) => {
|
|
const transaction = await ctx.app.fs.dc.orm.transaction();
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { solutionId } = ctx.params;
|
|
const { content } = ctx.request.body;
|
|
if (!solutionId || !content) { throw '参数错误'; }
|
|
|
|
await saveDocVersion(models, {
|
|
solutionId,
|
|
content,
|
|
chapterSnapshot: null,
|
|
triggerType: 'manual_save',
|
|
promptSnapshot: { source: 'saveDocEdit' },
|
|
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.reWriteDoc = async (ctx, next) => {
|
|
const transaction = await ctx.app.fs.dc.orm.transaction();
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { solutionId } = ctx.params;
|
|
const { prompt } = ctx.request.body;
|
|
if (!solutionId) { throw '缺少参数: solutionId'; }
|
|
|
|
const solution = await models.PatentSolutions.findOne({
|
|
where: { id: Number(solutionId) },
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
if (!solution) { throw '方案不存在'; }
|
|
|
|
const chapters = await models.PatentSolutionChapters.findAll({
|
|
where: { solutionId: Number(solutionId) },
|
|
order: [['sortOrder', 'ASC'], ['id', 'ASC']],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
const chapterTreeText = buildChapterStructure(chapters);
|
|
|
|
const baseFields = await models.PatentSolutionBaseFields.findAll({
|
|
where: { solutionId: Number(solutionId), isSelected: true },
|
|
order: [['sortOrder', 'ASC'], ['id', 'ASC']],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
|
|
const baseFieldText = baseFields.map(item => `${item.fieldTitle}:${item.fieldValue || ''}`).join('\n');
|
|
const projectInfoText = toProjectInfoText(solution.projectInfo);
|
|
|
|
const variables = {
|
|
generation_type: '内容生成',
|
|
};
|
|
if (projectInfoText) variables.projectInfo = projectInfoText;
|
|
if (solution.globalRequirements) variables.globalRequirements = solution.globalRequirements;
|
|
if (chapterTreeText) variables.chapters = chapterTreeText;
|
|
|
|
const userPrompt = prompt || '请基于当前目录和基础信息重新生成完整正文';
|
|
const sendText = [
|
|
userPrompt,
|
|
chapterTreeText ? `目录:\n${chapterTreeText}` : '',
|
|
baseFieldText ? `基础信息:\n${baseFieldText}` : '',
|
|
].filter(Boolean).join('\n\n');
|
|
|
|
const content = await callFastGpt(ctx, {
|
|
variables,
|
|
messages: [{ role: 'user', content: [{ type: 'text', text: sendText }] }],
|
|
userId: solution.creator,
|
|
actionId: `patent:${solutionId}:rewrite:doc:${Date.now()}`,
|
|
reportContext: { action: 'rewriteDoc', solutionId },
|
|
});
|
|
|
|
await saveDocVersion(models, {
|
|
solutionId,
|
|
content,
|
|
chapterSnapshot: chapters,
|
|
triggerType: 'rewrite',
|
|
promptSnapshot: { variables, sendText },
|
|
transaction,
|
|
});
|
|
|
|
await transaction.commit();
|
|
ctx.body = { content };
|
|
ctx.status = 200;
|
|
} catch (error) {
|
|
await transaction.rollback();
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = { message: typeof error === 'string' ? error : '重编正文失败' };
|
|
}
|
|
};
|
|
|
|
module.exports.restoreDocVersion = async (ctx, next) => {
|
|
const transaction = await ctx.app.fs.dc.orm.transaction();
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { solutionId, version } = ctx.params;
|
|
|
|
if (!solutionId || !version) { throw '缺少参数'; }
|
|
|
|
const target = await models.PatentSolutionDocVersions.findOne({
|
|
where: { solutionId: Number(solutionId), version: Number(version) },
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
if (!target) { throw '目标版本不存在'; }
|
|
|
|
await saveDocVersion(models, {
|
|
solutionId,
|
|
content: target.content,
|
|
chapterSnapshot: target.chapterSnapshot,
|
|
triggerType: 'restore',
|
|
fromVersion: Number(version),
|
|
promptSnapshot: { source: 'restoreDocVersion' },
|
|
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.patentChat = async (ctx, next) => {
|
|
const transaction = await ctx.app.fs.dc.orm.transaction();
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { solutionId } = ctx.params;
|
|
const { question, generationType = '前期对话', model, fileUrlList = [] } = ctx.request.body;
|
|
|
|
if (!solutionId || !question) { throw '缺少参数'; }
|
|
|
|
const solution = await models.PatentSolutions.findOne({
|
|
where: { id: Number(solutionId) },
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
if (!solution) { throw '方案不存在'; }
|
|
|
|
const maxRound = await models.PatentSolutionChatRecords.max('roundNo', {
|
|
where: { solutionId: Number(solutionId) },
|
|
transaction,
|
|
});
|
|
const roundNo = Number(maxRound || 0) + 1;
|
|
|
|
const variables = {
|
|
generation_type: generationType,
|
|
};
|
|
if (model) variables.model = model;
|
|
if (solution.projectInfo) variables.projectInfo = toProjectInfoText(solution.projectInfo);
|
|
if (solution.globalRequirements) variables.globalRequirements = solution.globalRequirements;
|
|
|
|
const content = [{ type: 'text', text: question }];
|
|
if (Array.isArray(fileUrlList) && fileUrlList.length) {
|
|
fileUrlList.forEach(url => {
|
|
if (url) {
|
|
content.push({ type: 'file_url', url });
|
|
}
|
|
});
|
|
}
|
|
|
|
const answer = await callFastGpt(ctx, {
|
|
variables,
|
|
messages: [{ role: 'user', content }],
|
|
userId: solution.creator,
|
|
actionId: `patent:${solutionId}:chat:${roundNo}:${Date.now()}`,
|
|
reportContext: {
|
|
action: 'patentChat',
|
|
solutionId,
|
|
roundNo,
|
|
},
|
|
});
|
|
|
|
const parsedFields = parseAnswerFields(answer);
|
|
|
|
await models.PatentSolutionChatRecords.create({
|
|
solutionId: Number(solutionId),
|
|
roundNo,
|
|
role: 'user',
|
|
generationType,
|
|
content: question,
|
|
}, { transaction });
|
|
|
|
const assistantRow = await models.PatentSolutionChatRecords.create({
|
|
solutionId: Number(solutionId),
|
|
roundNo,
|
|
role: 'assistant',
|
|
generationType,
|
|
content: answer,
|
|
parsedFields,
|
|
}, { transaction });
|
|
|
|
const oldMessages = Array.isArray(solution.messages) ? solution.messages : [];
|
|
oldMessages.push({ role: 'user', content: question });
|
|
oldMessages.push({ role: 'assistant', content: answer });
|
|
|
|
await models.PatentSolutions.update({
|
|
messages: oldMessages,
|
|
updateAt: moment(),
|
|
}, {
|
|
where: { id: Number(solutionId) },
|
|
transaction,
|
|
});
|
|
|
|
await transaction.commit();
|
|
ctx.body = {
|
|
answer,
|
|
parsedFields,
|
|
assistantChatId: assistantRow.id,
|
|
};
|
|
ctx.status = 200;
|
|
} catch (error) {
|
|
await transaction.rollback();
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error === 'string' ? error : '问答失败'
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports.getChatRecords = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { solutionId } = ctx.params;
|
|
if (!solutionId) { throw '缺少参数: solutionId'; }
|
|
|
|
const records = await models.PatentSolutionChatRecords.findAll({
|
|
where: { solutionId: Number(solutionId) },
|
|
order: [['roundNo', 'ASC'], ['id', 'ASC']],
|
|
raw: true,
|
|
});
|
|
|
|
ctx.body = records;
|
|
ctx.status = 200;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error === 'string' ? error : '获取问答记录失败'
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports.PATENT_STATUS_ENUM = PATENT_STATUS_ENUM;
|
|
|