ai-query对接新版freesun-agent接口的分支
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

718 lines
25 KiB

'use strict';
const superagent = require('superagent');
const { spawn } = require('child_process');
const stream = require('stream');
const path = require('path');
const moment = require('moment');
module.exports.getTenderList = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const { page, pageSize, userId } = ctx.request.query;
const where = {};
if (userId) {
where.userId = userId;
}
// 处理分页
const options = {
where,
order: [['updateAt', 'DESC']],
raw: true,
};
if (page && pageSize) {
options.offset = (page - 1) * pageSize;
options.limit = parseInt(pageSize);
}
const tenderList = await models.Tender.findAndCountAll(options);
ctx.body = tenderList;
ctx.status = 200;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : '获取标书列表失败'
};
}
}
module.exports.getTenderDetail = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const tenderId = ctx.params.tenderId;
if (!tenderId) { throw '缺少参数' };
const tender = await models.Tender.findOne({
where: { id: tenderId },
include: [
{
model: models.TenderDirectory,
}
],
});
ctx.body = tender;
ctx.status = 200;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : '获取标书详情失败'
};
}
}
module.exports.addTender = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const { name, fileUrl, onlyTechnicalPapers, userId, curIp } = ctx.request.body;
if (!name || !fileUrl) { throw '缺少参数' };
const tender = await models.Tender.create(
{ name, fileUrl, status: 'pending', userId },
{ returning: true }
);
await models.AiQueryRecord.create({
userId: userId,
ipAddress: curIp,
clientId: 'pep-feixiaoshang',
feature: '标书生成',
time: moment(),
});
ctx.body = tender;
ctx.status = 200;
// 执行生成目录任务,不影响此接口返回
generationDirectory(ctx, tender.id, fileUrl, onlyTechnicalPapers);
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : '新增标书失败'
};
}
}
module.exports.retryGenerationDirectory = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const id = ctx.params.tenderId;
if (!id) { throw '缺少参数' };
const tender = await models.Tender.update(
{ status: 'pending' },
{ where: { id }, returning: true }
);
ctx.body = tender;
ctx.status = 200;
// 执行生成目录任务,不影响此接口返回
generationDirectory(ctx, id, tender[1][0].fileUrl, tender[1][0].onlyTechnicalPapers);
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : '重新生成目录失败'
};
}
}
async function generationDirectory(ctx, tenderId, fileUrl, onlyTechnicalPapers = false) {
const transaction = await ctx.app.fs.dc.orm.transaction();
const { models } = ctx.app.fs.dc;
try {
const { tenderAppKey } = ctx.app.fs.config.fastGpt;
const res = await superagent
.post(`${ctx.app.fs.config.fastGpt.apiUrl}/api/v1/chat/completions`)
.send({
"stream": false,
"detail": false,
"variables": { "generation_type": onlyTechnicalPapers ? '技术文件目录生成' : '目录生成' },
"messages": [
{
"role": "user",
"content": [
{
"type": "file_url",
"name": "文件",
"url": fileUrl
}
]
}
]
})
.set({
Authorization: `Bearer ${tenderAppKey}`,
"Content-Type": "application/json",
})
// .timeout(1000 * 60 * 24)
.timeout({
response: 1000 * 60 * 24, // 等待服务器发送数据的超时时间(毫秒)
deadline: 1000 * 60 * 24, // 整个请求完成的最大时间(毫秒)
}).then(async res => {
const directoryMd = res.body.choices[0].message.content;
await models.Tender.update(
{ status: 'success' },
{ where: { id: tenderId }, transaction }
);
await parseAndInsertDirectories(directoryMd, models, transaction, tenderId);
transaction.commit();
}, async error => {
await models.Tender.update(
{ status: 'fail' },
{ where: { id: tenderId } }
);
ctx.logger.log(error);
await transaction.rollback();
})
// const directoryMd = res.body.choices[0].message.content;
// await new Promise((resolve) => { setTimeout(resolve, 5000) });
// const directoryMd = "\n# 1. 投标书 \n# 2. 开标一览表 \n# 3. 分项报价表 \n# 4. 开标一览明细表 \n# 5. 服务标准及要求响应/偏离表 \n# 6. 商务条款响应/偏离表 \n# 7. 投标人应当提交的资格证明文件 \n## 7.1 江西省政府采购供应商资格信用承诺函 \n## 7.2 法定代表人授权书 \n## 7.3 投标人的资格声明 \n## 7.4 投标保证金凭证 \n## 7.5 制造商出具的授权函 \n## 7.6 联合体协议 \n## 7.7 本项目的特定资格证明材料 \n# 8. 为落实政府采购政策投标人须提供的证明材料 \n## 8.1 中小企业声明(服务) \n## 8.2 省级以上监狱管理局、戒毒管理局出具的属于监狱企业证明文件 \n## 8.3 残疾人福利性单位声明函 \n## 8.4 节能产品认证证书 \n# 9. 技术文件 \n## 9.1 对项目的理解 \n### 9.1.1 采购需求及项目背景 \n#### 9.1.1.1 采购需求 \n#### 9.1.1.2 建设背景 \n#### 9.1.1.3 建设目标 \n### 9.1.2 实施内容理解 \n#### 9.1.2.1 升级数据资源中心 \n##### 9.1.2.1.1 数据目录系统升级 \n##### 9.1.2.1.2 数据库管控 \n##### 9.1.2.1.3 数据治理系统升级 \n###### 9.1.2.1.3.1 数据处理系统 \n###### 9.1.2.1.3.2 数据标准管理 \n###### 9.1.2.1.3.3 数据质量管理 \n###### 9.1.2.1.3.4 元数据管理 \n###### 9.1.2.1.3.5 数据建模管理 \n###### 9.1.2.1.3.6 标签管理 \n###### 9.1.2.1.3.7 算法开发 \n#### 9.1.2.1.4 数据服务平台 \n#### 9.1.2.1.5 数据技术规范 \n#### 9.1.2.1.6 数据库能力 \n#### 9.1.2.2 技术支撑中心 \n##### 9.1.2.2.1 能力组件中心 \n##### 9.1.2.2.2 能力开放中心 \n##### 9.1.2.2.3 安全管控中心 \n##### 9.1.2.2.4 能力技术规范 \n#### 9.1.2.3 一体化服务门户 \n##### 9.1.2.3.1 工作门户 \n#### 9.1.2.4 国产化适配 \n### 9.1.3 满足采购需求外的技术功能和软件产品能力 \n#### 9.1.3.1 技术功能 \n#### 9.1.3.2 软件产品能力 \n## 9.2 工作规划描述 \n### 9.2.1 需求分析 \n### 9.2.2 总体设计 \n### 9.2.3 详细设计 \n### 9.2.4 软件实现 \n### 9.2.5 软件测试 \n### 9.2.6 用户培训 \n### 9.2.7 系统上线 \n### 9.2.8 项目验收 \n## 9.3 项目测试方案 \n### 9.3.1 测试目标和原则 \n### 9.3.2 测试内容 \n### 9.3.3 测试程序 \n## 9.4 质量保障 \n### 9.4.1 质量保障体系 \n### 9.4.2 质量保障措施 \n### 9.4.3 阶段评审 \n### 9.4.4 日常检查 \n### 9.4.5 安装维护检查 \n### 9.4.6 文档 \n### 9.4.7 软件系统、设备的集成、调试质量控制 \n## 9.5 项目管理机构及人员 \n### 9.5.1 项目管理组织机构 \n### 9.5.2 项目人员配置及分工 \n### 9.5.3 岗位职责及工作界面划分 \n## 9.6 培训计划和培训方案 \n### 9.6.1 培训计划 \n### 9.6.2 培训方案 \n#### 9.6.2.1 培训目标 \n#### 9.6.2.2 培训内容 \n#### 9.6.2.3 培训质量保证措施 \n## 9.7 售后服务与支撑 \n### 9.7.1 售后服务承诺 \n### 9.7.2 运维服务承诺 \n### 9.7.3 服务人员承诺 \n## 9.8 知识产权 \n## 9.9 项目保密 \n# 10. 其他资料"
} catch (error) {
// await transaction.rollback();
ctx.logger.log(error);
try {
await models.Tender.update(
{ status: 'fail' },
{ where: { id: tenderId } }
);
} catch (err) {
console.log(`修改状态失败,tender.id=${tenderId}`);
ctx.logger.log(err);
}
}
}
// 解析并插入目录树
async function parseAndInsertDirectories(input, models, transaction, tenderId) {
// 删除原目录
await models.TenderDirectory.destroy({ where: { tenderId }, transaction });
// 1. 解析输入字符串为树形结构
const lines = input.split('\n').filter(line => line.trim() !== '');
const tree = [];
const stack = [];
for (const line of lines) {
// 提取编号和名称
const match = line.match(/^(#+)\s+([\d.]+\s+.*)/);
if (!match) continue;
const level = match[1].length; // 计算层级
const fullName = match[2].trim(); // 获取完整名称(包括编号)
// 构建当前节点
const node = { level, fullName, children: [] };
// 根据层级插入到树中
while (stack.length > 0 && stack[stack.length - 1].level >= level) {
stack.pop(); // 回退到上一级
}
if (stack.length === 0) {
tree.push(node); // 如果栈为空,说明是根节点
} else {
stack[stack.length - 1].children.push(node); // 否则作为子节点添加
}
stack.push(node); // 当前节点入栈
}
// 2. 递归插入目录树到数据库
async function insertNode(node, parentId = null, order = 1) {
// 插入当前节点
const directory = await models.TenderDirectory.create({
tenderId,
name: node.fullName, // 使用完整名称(包含编号)
parentId,
order,
}, { transaction });
// 插入子节点
for (let i = 0; i < node.children.length; i++) {
await insertNode(node.children[i], directory.id, i + 1); // 子节点的 order 从 1 开始
}
}
// 遍历树并插入所有节点
for (let i = 0; i < tree.length; i++) {
await insertNode(tree[i], null, i + 1); // 根节点的 order 从 1 开始
}
}
module.exports.putTender = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const tenderId = ctx.params.tenderId;
const body = ctx.request.body;
if (!tenderId) { throw '缺少参数' };
await models.Tender.update(
{ ...body },
{ where: { id: tenderId } }
);
ctx.status = 204;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : '修改标书失败'
};
}
}
module.exports.delTender = async (ctx, next) => {
const transaction = await ctx.app.fs.dc.orm.transaction();
try {
const { models } = ctx.app.fs.dc;
const tenderId = ctx.params.tenderId;
if (!tenderId) { throw '缺少参数' };
await models.TenderDirectory.destroy({
where: { tenderId },
transaction
});
await models.Tender.destroy({
where: { id: tenderId },
transaction
});
ctx.status = 204;
await transaction.commit();
} catch (error) {
await transaction.rollback();
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : '删除标书失败'
};
}
}
module.exports.exportTender = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const tenderId = ctx.params.tenderId;
if (!tenderId) { throw '缺少参数' };
const tender = await models.Tender.findOne({
where: { id: tenderId },
attributes: ['id', 'name'],
raw: true,
});
const tenderDirectories = await models.TenderDirectory.findAll({
where: { tenderId },
attributes: ['id', 'name', 'content', 'parentId', 'order'],
raw: true,
});
const tree = buildTree(tenderDirectories);
const mdContent = generateMdFromTree(tree);
// 调用 Pandoc 将 Markdown 字符串转换为 DOCX
const pandoc = spawn('pandoc', [
'-f', 'markdown', // -f markdown: 指定输入格式为 markdown
'-t', 'docx', // 指定输出格式为 docx
'--toc', // 生成目录
'--metadata', 'toc-title=目录', // 元数据:目录名
'--reference-doc', path.resolve(__dirname, '../static/custom-reference.docx'), // 格式参考文档
'-o', '-' // 指定输出到 stdout (标准输出)
]);
// 创建一个可写流来捕获 Pandoc 的 stdout
let docxBuffers = [];
pandoc.stdout.on('data', (data) => {
docxBuffers.push(data);
});
// 处理 Pandoc 进程的错误
let pandocError = '';
pandoc.stderr.on('data', (data) => {
pandocError += data.toString();
});
// 将 Markdown 字符串写入 Pandoc 的 stdin
const stdinStream = new stream.Readable();
stdinStream.push(mdContent);
stdinStream.push(null); // 表示输入结束
stdinStream.pipe(pandoc.stdin);
// 等待 Pandoc 进程结束
await new Promise((resolve, reject) => {
pandoc.on('close', (code) => {
if (code !== 0) {
console.error('Pandoc stderr:', pandocError);
reject(new Error(`Pandoc exited with code ${code}. Error: ${pandocError}`));
} else {
resolve();
}
});
pandoc.on('error', (err) => {
console.error('Failed to start Pandoc process:', err);
reject(err);
});
});
const docxBuffer = Buffer.concat(docxBuffers);
// 设置响应头,告诉浏览器这是一个 DOCX 文件并应该下载
const fileName = encodeURI(`${tender.name}.docx`);
ctx.set('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
ctx.set('Content-Disposition', `attachment; filename="${fileName}"`);
ctx.set('Content-Length', docxBuffer.length.toString());
ctx.body = docxBuffer;
ctx.status = 200;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : '导出标书失败'
};
}
}
function buildTree(items) {
const map = {};
const tree = [];
// 构建 ID 到节点的映射
items.forEach(item => {
map[item.id] = { ...item, children: [] };
});
// 构建树结构
items.forEach(item => {
const node = map[item.id];
if (item.parentId && map[item.parentId]) {
map[item.parentId].children.push(node);
} else if (!item.parentId) {
tree.push(node);
}
});
// 按 order 排序
function sortNode(node) {
node.children.sort((a, b) => a.order - b.order);
node.children.forEach(sortNode);
}
tree.sort((a, b) => a.order - b.order);
tree.forEach(sortNode);
return tree;
}
function generateMdFromTree(tree, level = 1) {
let mdContent = '';
function traverse(node, currentLevel) {
const heading = '#'.repeat(currentLevel);
mdContent += `${heading} ${node.name}\n${node.content || ''}\n\n`;
if (node.children.length > 0) {
node.children.forEach(child => traverse(child, currentLevel + 1));
}
}
tree.forEach(node => traverse(node, level));
return mdContent.trim();
}
module.exports.contentGenerate = async (ctx, next) => {
try {
const { tenderAppKey } = ctx.app.fs.config.fastGpt;
const { models, orm: sequelize } = ctx.app.fs.dc;
const { tenderId } = ctx.params;
const { directoryId, fileUrl } = ctx.request.body;
if (!tenderId || !directoryId || !fileUrl) { throw '缺少请求参数' };
let text = '';
const result = await sequelize.query(`
WITH RECURSIVE parent_tree AS (
SELECT id, name, parent_id
FROM tender_directory
WHERE id = ${directoryId}
UNION ALL
SELECT d.id, d.name, d.parent_id
FROM tender_directory d
INNER JOIN parent_tree pt ON d.id = pt.parent_id
)
SELECT string_agg(name, '-' ORDER BY id ASC) AS path
FROM parent_tree;
`, { type: sequelize.QueryTypes.SELECT });
if (result.length > 0) {
text = '章节:' + result[0].path;
} else {
throw '章节查询错误';
}
const curDirectory = await models.TenderDirectory.findOne({
where: {
id: directoryId,
tenderId
},
attributes: ['chapterRequirements', 'chapterWords'],
raw: true
});
let variables = {};
if (curDirectory?.chapterRequirements) {
text = text + '\n章节需求:' + curDirectory.chapterRequirements;
}
if (typeof curDirectory?.chapterWords === 'number') {
variables = { word_count_requirement: curDirectory.chapterWords };
}
const res = await superagent
.post(`${ctx.app.fs.config.fastGpt.apiUrl}/api/v1/chat/completions`)
.send({
"stream": false,
"detail": false,
"variables": variables,
"messages": [
{
"role": "user",
"content": [
{
"type": "file_url",
"name": "文件",
"url": fileUrl
},
{
"type": "text",
"text": text
}
]
}
]
})
.set({
Authorization: `Bearer ${tenderAppKey}`,
"Content-Type": "application/json",
})
const contentMd = res.body.choices[0].message.content;
await models.TenderDirectory.update(
{ content: contentMd },
{ where: { id: directoryId } }
);
ctx.body = contentMd;
ctx.status = 200;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : '章节内容生成失败'
};
}
}
// 内容润色
module.exports.contentEmbellish = async (ctx, next) => {
try {
const { tenderAppKey } = ctx.app.fs.config.fastGpt;
const { tenderId } = ctx.params;
const { directoryId, fileUrl, selectedText, embellishDemand } = ctx.request.body;
if (!tenderId || !directoryId || !fileUrl || !selectedText || !embellishDemand) {
throw '缺少请求参数'
};
const res = await superagent
.post(`${ctx.app.fs.config.fastGpt.apiUrl}/api/v1/chat/completions`)
.send({
"stream": false,
"detail": false,
"variables": { "generation_type": '章节内容修改' },
"messages": [
{
"role": "user",
"content": [
{
"type": "file_url",
"name": "文件",
"url": fileUrl
},
{
"type": "text",
"text": `待优化段落:\n${selectedText}\n\n用户需求:\n${embellishDemand}\n`
}
]
}
]
})
.set({
Authorization: `Bearer ${tenderAppKey}`,
"Content-Type": "application/json",
})
const contentMd = res.body.choices[0].message.content;
ctx.body = contentMd;
ctx.status = 200;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : '章节文字润色失败'
};
}
}
module.exports.addTenderDirectory = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const { tenderId } = ctx.params;
const body = ctx.request.body;
if (!tenderId || !body) { throw '缺少参数' };
await models.TenderDirectory.create(body);
ctx.status = 204;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : '添加章节失败'
};
}
}
// 修改节点顺序
async function moveNode(ctx, tenderId, id, direction, transaction) {
const { models } = ctx.app.fs.dc;
// 1. 获取目标节点的父节点 ID 和当前顺序
const targetNode = await models.TenderDirectory.findByPk(id, {
attributes: ["parentId", "order"],
});
if (!targetNode) {
throw new Error(`Node with id ${id} not found`);
}
const { parentId, order: currentOrder } = targetNode;
// 2. 获取同级节点(根据 parentId 分组)
const siblings = await models.TenderDirectory.findAll({
attributes: ["id", "order"],
where: {
parentId,
tenderId,
},
order: [["order", "ASC"]],
raw: true,
});
// 找到目标节点在同级节点中的索引
const targetIndex = siblings.findIndex(node => node.id == id);
if (targetIndex === -1) {
throw new Error(`Node with id ${id} not found in siblings`);
}
// 3. 根据方向计算交换的目标节点
let swapIndex;
if (direction === "up") {
if (targetIndex === 0) {
console.log("Already at the top, cannot move up further.");
return; // 已经是第一个节点,无法继续上移
}
swapIndex = targetIndex - 1; // 上移:与前一个节点交换顺序
} else if (direction === "down") {
if (targetIndex === siblings.length - 1) {
console.log("Already at the bottom, cannot move down further.");
return; // 已经是最后一个节点,无法继续下移
}
swapIndex = targetIndex + 1; // 下移:与后一个节点交换顺序
} else {
throw new Error("Invalid direction. Use 'up' or 'down'.");
}
// 4. 获取需要交换顺序的节点
const swapNode = siblings[swapIndex];
// 5. 交换两个节点的顺序值
await models.TenderDirectory.update(
{ order: swapNode.order },
{ where: { id }, transaction }
);
await models.TenderDirectory.update(
{ order: currentOrder },
{ where: { id: swapNode.id }, transaction }
);
console.log(`Moved node ${id} ${direction}`);
}
module.exports.putTenderDirectory = async (ctx, next) => {
const transaction = await ctx.app.fs.dc.orm.transaction();
try {
const { models } = ctx.app.fs.dc;
const { tenderId, directoryId } = ctx.params;
const body = ctx.request.body;
if (!tenderId || !directoryId) { throw '缺少参数' };
if (body.direction) {
await moveNode(ctx, tenderId, directoryId, body.direction, transaction);
} else {
await models.TenderDirectory.update(
{ ...body },
{
where: {
id: directoryId,
tenderId
},
transaction
}
);
}
ctx.status = 204;
await transaction.commit();
} catch (error) {
await transaction.rollback();
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : '修改章节失败'
};
}
}
module.exports.delTenderDirectory = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const Op = ctx.app.fs.dc.ORM.Op;
const { tenderId, directoryId } = ctx.params;
if (!tenderId || !directoryId) { throw '缺少参数' };
// 递归删除所有子章节
async function getChildIds(parentId, allChildIds = []) {
const children = await models.TenderDirectory.findAll({
attributes: ["id"], // 只查询 id 字段
where: { parentId },
});
if (children.length > 0) {
const childIds = children.map(child => child.id);
allChildIds.push(...childIds);
// 对每个子节点递归查找其子节点
for (const childId of childIds) {
await getChildIds(childId, allChildIds);
}
}
return allChildIds;
}
// 获取目标节点及其所有子节点的 ID
const allIdsToDelete = [Number(directoryId)]; // 包含目标节点本身
const childIds = await getChildIds(directoryId);
allIdsToDelete.push(...childIds);
console.log(allIdsToDelete, 'allIdsToDelete')
await models.TenderDirectory.destroy({
where: {
id: { [Op.in]: allIdsToDelete },
tenderId
},
});
ctx.status = 204;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : '删除章节失败'
};
}
}