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.
 
 
 

543 lines
21 KiB

'use strict';
const qiniu = require('qiniu');
const moment = require('moment');
const XLSX = require('xlsx-js-style');
const superagent = require('superagent');
const { convertHTMLToDOCX } = require('../utils/tools');
const getQwenUsage = (body = {}) => {
const usage = body?.usage || {};
const inputTokens = Number(usage.prompt_tokens || usage.input_tokens || 0);
const outputTokens = Number(usage.completion_tokens || usage.output_tokens || 0);
const totalTokens = Number(usage.total_tokens || inputTokens + outputTokens || 0);
return {
inputTokens: Math.max(0, Math.floor(inputTokens)),
outputTokens: Math.max(0, Math.floor(outputTokens)),
totalTokens: Math.max(0, Math.floor(totalTokens)),
};
};
const reportQuoteOptimizeUsage = async (ctx, { body, runningTime, quoteData }) => {
const usage = getQwenUsage(body);
if (!usage.totalTokens) return;
const centerRequest = ctx?.app?.fs?.centerRequest;
if (!centerRequest?.post) {
ctx.logger?.warn?.('ai-center token统计接口未初始化,跳过引用优化token上报');
return;
}
try {
await centerRequest.post('/chatLogs/report', {
body: {
app_id: 'ai-query',
chat_item_data_id: String(quoteData?._id || quoteData?.id || ''),
module_type: 'quote_optimize',
fastgpt_source: 'qwen',
origin_ip: ctx.ip || '',
running_time: runningTime,
duration_seconds: runningTime,
input_tokens: usage.inputTokens,
output_tokens: usage.outputTokens,
total_tokens: usage.totalTokens,
source_platform: 'ai-query',
source_key: 'aichat_quote_optimize',
model: body?.model || 'qwen3.6-flash'
}
});
} catch (error) {
ctx.logger?.error?.('引用优化token上报失败:', error);
}
};
//[转发知识库列表]
module.exports.getKnowledgeList = async (ctx, next) => {
try {
const { header, query } = ctx;
const token = query.token;
const {knowledgeUrl} = ctx.config.aiCenterUrl;
const requestQuery = {
page: query.page || 1,
pageSize: query.pageSize || 12,
parentId: query.parentId === undefined ? 'null' : query.parentId,
};
if (query.keyword) requestQuery.keyword = query.keyword;
if (query.search) requestQuery.search = query.search;
if (query.userId) requestQuery.userId = query.userId;
if (query.type) requestQuery.type = query.type;
const res = await superagent
.get(knowledgeUrl)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.set('token', token)
.query(requestQuery);
ctx.status = res.status || 200;
ctx.body = res.body;
} catch (error) {
const status = error?.status || error?.response?.status || 400;
const body = error?.response?.body;
const text = error?.response?.text;
ctx.logger.error('转发知识库列表失败:', error);
ctx.status = status;
ctx.body = body || {
message: text || error.message || '获取知识库列表失败',
};
}
};
module.exports.downloadScheme = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const { qiniu: { dmn: domain, bkt: bucket, ak: accessKey, sk: secretKey } } = ctx.config;
const { content, dataId, OS } = ctx.request.body;
if (!content || !dataId) { throw '缺少参数' };
if (OS === 'android' || OS === 'ios') {
let result;
const cache = await models.ChatSchemeFileCache.findOne({ where: { dataId }, raw: true });
if (cache) {
result = cache;
} else {
const docxBuffer = await convertHTMLToDOCX(content);
// 上传七牛云
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
const options = { scope: bucket };
const putPolicy = new qiniu.rs.PutPolicy(options);
const uploadToken = putPolicy.uploadToken(mac);
const formUploader = new qiniu.form_up.FormUploader();
const putExtra = new qiniu.form_up.PutExtra();
const key = `ai-query/chat-schema/${moment().format('YYYYMMDDHHmmss')}/方案-${dataId}.docx`;
const config = new qiniu.conf.Config();
config.zone = qiniu.zone.Zone_z0;
const uploadResult = await new Promise((resolve, reject) => {
formUploader.put(uploadToken, key, docxBuffer, putExtra, (respErr, respBody, respInfo) => {
if (respErr) {
reject(respErr);
} else {
resolve(respBody);
}
})
})
// 保存到数据库
const { key: qiniuKey } = uploadResult;
const url = `${domain}/${qiniuKey}`;
await models.ChatSchemeFileCache.create({ dataId, fileUrl: url });
result = { dataId, fileUrl: url };
}
ctx.set('Content-Type', 'application/json');
ctx.status = 200;
ctx.body = result;
} else {
const docxBuffer = await convertHTMLToDOCX(content);
ctx.set('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
ctx.set('Content-Disposition', `attachment; filename=${encodeURIComponent(`方案`)}.docx`);
ctx.body = docxBuffer;
ctx.status = 200;
}
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : '下载失败'
};
}
}
module.exports.downloadWorkingHoursEstimate = async (ctx, next) => {
try {
const { body } = ctx.request;
const { qiniu: { dmn: domain, bkt: bucket, ak: accessKey, sk: secretKey } } = ctx.config;
// 验证数据完整性
if (!body || !body.project_overview || !body.requirement_breakdown || !body.estimation_summary) {
throw '缺少必要的数据字段';
}
// 创建新的工作簿
const workbook = XLSX.utils.book_new();
// 创建项目概览工作表
const projectOverviewData = [
['项目概览'],
[''],
['项目名称', body.project_overview.project_name || ''],
['项目类型', body.project_overview.project_type || ''],
['预估规模', body.project_overview.estimated_scale || '']
];
const projectOverviewSheet = XLSX.utils.aoa_to_sheet(projectOverviewData);
// 设置样式
projectOverviewSheet['A1'] = {
v: '项目概览',
s: {
font: { bold: true, sz: 16, color: { rgb: "FFFFFF" } },
fill: { bgColor: { indexed: 64 }, fgColor: { rgb: "4F81BD" } },
alignment: { horizontal: "center", vertical: "center" }
}
};
// 设置列宽
projectOverviewSheet['!cols'] = [{ width: 15 }, { width: 30 }];
// 合并标题单元格
projectOverviewSheet['!merges'] = [{ s: { r: 0, c: 0 }, e: { r: 0, c: 1 } }];
XLSX.utils.book_append_sheet(workbook, projectOverviewSheet, '项目概览');
// 创建需求分解工作表
const requirementData = [
['需求分解详情'],
[''],
['业务域', '功能单元', '任务描述', '参考任务描述', '管理/h', '研发/h', '测试/h', '任务工时统计/h', '功能工时统计/h', '域工时统计/h']
];
// 填充需求分解数据
let mergeRanges = []; // 用于存储需要合并的单元格范围
let totalManagementHours = 0;
let totalDevelopmentHours = 0;
let totalTestingHours = 0;
let totalProjectHours = 0;
let totalUnitHours = 0; // 功能工时统计总和
let totalDomainHours = 0; // 域工时统计总和
body.requirement_breakdown.forEach((domain, domainIndex) => {
let domainStartRow = requirementData.length;
let domainTaskCount = 0;
let domainManagementHours = 0;
let domainDevelopmentHours = 0;
let domainTestingHours = 0;
// 累加域工时到总计
totalDomainHours += domain.domain_total_hours || 0;
domain.functional_units.forEach((unit, unitIndex) => {
let unitStartRow = requirementData.length;
let unitManagementHours = 0;
let unitDevelopmentHours = 0;
let unitTestingHours = 0;
// 累加功能单元工时到总计
totalUnitHours += unit.unit_total_hours || 0;
unit.tasks.forEach((task, taskIndex) => {
const managementHours = task.management?.subtotal_hours || 0;
const developmentHours = task.development?.subtotal_hours || 0;
const testingHours = task.testing?.subtotal_hours || 0;
// 累加各级别的工时
unitManagementHours += managementHours;
unitDevelopmentHours += developmentHours;
unitTestingHours += testingHours;
domainManagementHours += managementHours;
domainDevelopmentHours += developmentHours;
domainTestingHours += testingHours;
totalManagementHours += managementHours;
totalDevelopmentHours += developmentHours;
totalTestingHours += testingHours;
requirementData.push([
(unitIndex === 0 && taskIndex === 0) ? domain.business_domain : '',
(taskIndex === 0) ? unit.unit_name : '',
task.task_description || '',
task.reference_description || '',
managementHours,
developmentHours,
testingHours,
task.total_task_hours || 0,
(taskIndex === 0) ? (unit.unit_total_hours || 0) : '',
(unitIndex === 0 && taskIndex === 0) ? (domain.domain_total_hours || 0) : ''
]);
domainTaskCount++;
});
// 记录功能单元合并范围
if (unit.tasks.length > 1) {
mergeRanges.push({
s: { r: unitStartRow, c: 1 }, // 功能单元列
e: { r: unitStartRow + unit.tasks.length - 1, c: 1 }
});
mergeRanges.push({
s: { r: unitStartRow, c: 8 }, // 功能工时统计列
e: { r: unitStartRow + unit.tasks.length - 1, c: 8 }
});
}
});
// 记录业务域合并范围
if (domainTaskCount > 1) {
mergeRanges.push({
s: { r: domainStartRow, c: 0 }, // 业务域列
e: { r: domainStartRow + domainTaskCount - 1, c: 0 }
});
mergeRanges.push({
s: { r: domainStartRow, c: 9 }, // 域工时统计列
e: { r: domainStartRow + domainTaskCount - 1, c: 9 }
});
}
});
totalProjectHours = totalManagementHours + totalDevelopmentHours + totalTestingHours;
// 添加项目总计行
requirementData.push([
'总计', '', '', '',
totalManagementHours,
totalDevelopmentHours,
totalTestingHours,
totalProjectHours,
totalUnitHours,
totalDomainHours
]);
const requirementSheet = XLSX.utils.aoa_to_sheet(requirementData);
// 设置标题样式
requirementSheet['A1'] = {
v: '需求分解详情',
s: {
font: { bold: true, sz: 16, color: { rgb: "FFFFFF" } },
fill: { bgColor: { indexed: 64 }, fgColor: { rgb: "4F81BD" } },
alignment: { horizontal: "center", vertical: "center" }
}
};
// 设置表头样式
for (let col = 0; col < 10; col++) {
const cellRef = XLSX.utils.encode_cell({ r: 2, c: col });
if (requirementSheet[cellRef]) {
requirementSheet[cellRef].s = {
font: { bold: true, color: { rgb: "FFFFFF" } },
fill: { bgColor: { indexed: 64 }, fgColor: { rgb: "4F81BD" } },
alignment: { horizontal: "center", vertical: "center" },
border: {
top: { style: "thin", color: { rgb: "000000" } },
bottom: { style: "thin", color: { rgb: "000000" } },
left: { style: "thin", color: { rgb: "000000" } },
right: { style: "thin", color: { rgb: "000000" } }
}
};
}
}
// 设置数据行样式和边框
for (let row = 3; row < requirementData.length; row++) {
for (let col = 0; col < 10; col++) {
const cellRef = XLSX.utils.encode_cell({ r: row, c: col });
if (requirementSheet[cellRef]) {
const isTotalRow = requirementSheet[cellRef].v &&
requirementSheet[cellRef].v.toString() === '总计';
requirementSheet[cellRef].s = {
alignment: { horizontal: "left", vertical: "center" },
font: isTotalRow ? { bold: true } : {},
fill: isTotalRow ? { bgColor: { indexed: 64 }, fgColor: { rgb: "E6F2FF" } } : {},
border: {
top: { style: "thin", color: { rgb: "000000" } },
bottom: { style: "thin", color: { rgb: "000000" } },
left: { style: "thin", color: { rgb: "000000" } },
right: { style: "thin", color: { rgb: "000000" } }
}
};
}
}
}
// 设置列宽
requirementSheet['!cols'] = [
{ width: 15 }, { width: 20 }, { width: 35 }, { width: 30 },
{ width: 10 }, { width: 10 }, { width: 10 }, { width: 15 },
{ width: 15 }, { width: 15 }
];
// 合并标题单元格和数据单元格
requirementSheet['!merges'] = [
{ s: { r: 0, c: 0 }, e: { r: 0, c: 9 } }, // 标题行合并
...mergeRanges // 添加数据行的合并范围
]; XLSX.utils.book_append_sheet(workbook, requirementSheet, '需求分解');
// 创建评估汇总工作表
const summaryData = [
['评估汇总'],
[''],
['公司标准总工时', body.estimation_summary.company_standard_total_hours || 0],
[''],
['功能点分析'],
['内部逻辑文件数(ILF)', body.estimation_summary.function_point_analysis?.ilf_count || 0],
['外部接口文件数(EIF)', body.estimation_summary.function_point_analysis?.eif_count || 0],
['未调整功能点(UFP)', body.estimation_summary.function_point_analysis?.ufp || 0],
['软件类型因子', body.estimation_summary.function_point_analysis?.software_type_factor || 0],
['复用因子', body.estimation_summary.function_point_analysis?.reuse_factor || 0],
['调整后功能点', body.estimation_summary.function_point_analysis?.adjusted_fp || 0],
['基于功能点的工时', body.estimation_summary.function_point_analysis?.fp_based_hours || 0],
[''],
['最终推荐工时', body.estimation_summary.final_recommended_hours || 0],
['风险评估', body.estimation_summary.risk_assessment || '']
];
const summarySheet = XLSX.utils.aoa_to_sheet(summaryData);
// 设置样式
summarySheet['A1'] = {
v: '评估汇总',
s: {
font: { bold: true, sz: 16, color: { rgb: "FFFFFF" } },
fill: { bgColor: { indexed: 64 }, fgColor: { rgb: "4F81BD" } },
alignment: { horizontal: "center", vertical: "center" }
}
};
summarySheet['A5'] = {
v: '功能点分析',
s: {
font: { bold: true, sz: 14, color: { rgb: "FFFFFF" } },
fill: { bgColor: { indexed: 64 }, fgColor: { rgb: "92A9D1" } },
alignment: { horizontal: "center", vertical: "center" }
}
};
// 设置列宽
summarySheet['!cols'] = [{ width: 20 }, { width: 25 }];
// 合并标题单元格
summarySheet['!merges'] = [
{ s: { r: 0, c: 0 }, e: { r: 0, c: 1 } },
{ s: { r: 4, c: 0 }, e: { r: 4, c: 1 } }
];
XLSX.utils.book_append_sheet(workbook, summarySheet, '评估汇总');
// 生成XLSX缓冲区
const xlsxBuffer = XLSX.write(workbook, {
bookType: 'xlsx',
type: 'buffer',
cellStyles: true
});
// 上传到七牛云
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
const options = { scope: bucket };
const putPolicy = new qiniu.rs.PutPolicy(options);
const uploadToken = putPolicy.uploadToken(mac);
const formUploader = new qiniu.form_up.FormUploader();
const putExtra = new qiniu.form_up.PutExtra();
const timestamp = moment().format('YYYYMMDDHHmmss');
const key = `ai-query/working-hours-estimate/${timestamp}/工时评估-${body.project_overview.project_name || 'project'}.xlsx`;
const config = new qiniu.conf.Config();
config.zone = qiniu.zone.Zone_z0;
const uploadResult = await new Promise((resolve, reject) => {
formUploader.put(uploadToken, key, xlsxBuffer, putExtra, (respErr, respBody, respInfo) => {
if (respErr) {
reject(respErr);
} else {
resolve(respBody);
}
});
});
// 构造七牛云URL
const { key: qiniuKey } = uploadResult;
const url = `${domain}/${qiniuKey}`;
ctx.status = 200;
ctx.body = {
success: true,
message: '工时评估报告生成成功',
fileUrl: url,
fileName: `${body.project_overview.project_name || 'project'}.xlsx`
};
} catch (error) {
ctx.logger.error('生成工时评估报告失败:', error);
ctx.status = 400;
ctx.body = {
success: false,
message: typeof error === 'string' ? error : '生成工时评估报告失败',
error: error.message || error
};
}
}
//[优化引用数据]
module.exports.optimizeQuoteData = async (ctx, next) => {
try {
const startTime = Date.now();
const { quoteData } = ctx.request.body || {};
const content = quoteData?.q || quoteData?.content || '';
const sourceName = quoteData?.sourceName || quoteData?.collection?.name || '';
const updateTime = quoteData?.updateTime || quoteData?.collection?.updateTime || '';
const linkMeta = {
sourceName,
updateTime,
collectionId: quoteData?.collectionId || quoteData?.collection?.collection_id || quoteData?.collection?._id,
sourceId: quoteData?.sourceId,
datasetId: quoteData?.datasetId,
collection: quoteData?.collection
};
const apiUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions';
const appKey = ctx.app.fs.config.ali.appKey
if (!content) { throw '缺少引用内容' };
if (!appKey) { throw '千问优化应用未配置' };
const requestBody = {
model: 'qwen3.6-flash',
stream: false,
enable_thinking: false,
messages: [{
role: 'user',
content: [
'请把下面的引用正文整理为标准 Markdown,标题从三级标题(###)开始。',
'只允许结构化排版,不得增删事实内容,不得改写数字、单位、专有名词、时间和链接。',
'如正文中已有 Markdown 链接或 URL,必须原样保留 href/url,不要改写、补全或删除。',
'只返回整理后的 Markdown 正文,不要返回解释、代码块或 JSON。',
sourceName ? `文件名:${sourceName}` : '',
updateTime ? `时间:${updateTime}` : '',
'引用正文:',
content
].join('\n')
}]
};
const res = await superagent
.post(apiUrl)
.send(requestBody)
.set({
Authorization: `Bearer ${appKey}`,
'Content-Type': 'application/json'
});
const body = res.body || JSON.parse(res.text || '{}');
const contentMd = body?.choices?.[0]?.message?.content || '';
const runningTime = Number(((Date.now() - startTime) / 1000).toFixed(3));
reportQuoteOptimizeUsage(ctx, {
body,
runningTime,
quoteData
});
ctx.status = 200;
ctx.body = {
code: 200,
data: {
...quoteData,
...linkMeta,
q: contentMd || content,
content: contentMd,
originalQ: quoteData?.q,
originalContent: quoteData?.content,
raw: body
}
};
} catch (error) {
ctx.logger.error('优化引用数据失败:', error);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : '优化引用数据失败'
};
}
};