'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 = `
专利名称
技术联系人 电话 申请类型(发明、实用、外观)
发明人 本申请所属的技术领域(比如化妆品、汽车、电子通信、计算机)
专利权人 研发立项项目 关联产品
申请专利的目的 □保护, □干扰竞争对手, □对抗/进攻对手, □其他___________
`.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;