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.
 
 
 

266 lines
8.3 KiB

'use strict';
const superagent = require('superagent');
const { v4: uuidv4 } = require('uuid');
const { reportBusinessCall, reportFastgptResponse } = require('../../services/dashboardReporter');
const getFastGptKey = (app, explicitKey) => {
const config = app.fs.config.fastGpt || {};
return explicitKey || config.reportAppKey || '';
};
const requestFastGptTextWithApp = async (ctx, textContent, options = {}) => {
const app = ctx.app;
const config = app.fs.config.fastGpt || {};
if (!config.apiUrl) throw '未配置 fastGpt.apiUrl';
const key = getFastGptKey(app, options.appKey);
if (!key) throw '未配置 fastGpt.reportAppKey';
const payload = {
stream: false,
detail: true,
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: textContent,
}
],
}
],
};
if (options.variables) {
payload.variables = options.variables;
}
const res = await superagent
.post(`${config.apiUrl}/api/v1/chat/completions`)
.send(payload)
.set({
Authorization: `Bearer ${key}`,
'Content-Type': 'application/json',
});
await reportFastgptResponse({
ctx,
applicationId: 'exp-report',
actionId: options.actionId || `report:${res?.body?.responseData?.id || res?.body?.dataId || Date.now()}`,
responseBody: res?.body,
appKey: key,
userId: options.userId,
includeCall: false,
});
const content = res?.body?.choices?.[0]?.message?.content;
if (!content) throw 'fastgpt 返回内容为空';
return content;
};
const parseOutlineContent = (content = '') => {
const safeContent = String(content || '').trim();
if (!safeContent) return [];
try {
const parsed = JSON.parse(safeContent);
if (Array.isArray(parsed)) return parsed;
if (Array.isArray(parsed?.outline)) return parsed.outline;
if (Array.isArray(parsed?.data)) return parsed.data;
} catch (err) { }
return safeContent
.split(/\r?\n/)
.map(line => line.trim())
.filter(Boolean)
.map(line => line.replace(/^\d+[\.\)\-、\s]*/, '').trim())
.filter(Boolean)
.map(name => ({ name, child: [] }));
};
const normalizeFastGptMessageContent = (content) => {
if (typeof content === 'string') return content.trim();
if (Array.isArray(content)) {
return content
.map(item => {
if (typeof item === 'string') return item;
if (typeof item?.text === 'string') return item.text;
return '';
})
.filter(Boolean)
.join('\n')
.trim();
}
if (content && typeof content === 'object' && typeof content.text === 'string') {
return content.text.trim();
}
return '';
};
const normalizeOptionalString = value => {
const normalized = String(value || '').trim();
return normalized || null;
};
const summarizeChartSnapshot = snapshot => {
const seriesList = Array.isArray(snapshot?.seriesList) ? snapshot.seriesList : [];
if (!seriesList.length) return '';
const payload = seriesList.slice(0, 5).map(series => ({
name: normalizeOptionalString(series?.name) || '数据',
points: (Array.isArray(series?.points) ? series.points : []).slice(0, 30).map(point => ({
x: point?.x,
y: point?.y,
})),
}));
return `图表数据:\n${JSON.stringify(payload, null, 2)}`;
};
const summarizeTableSnapshot = snapshot => {
const columns = (Array.isArray(snapshot?.columns) ? snapshot.columns : [])
.map(item => normalizeOptionalString(item?.title || item?.label || item?.field || item?.sourceField))
.filter(Boolean);
const rows = Array.isArray(snapshot?.rows) ? snapshot.rows.slice(0, 20) : [];
const summary = snapshot?.summary || null;
return [
columns.length ? `表头: ${columns.join(' | ')}` : '',
rows.length ? `明细行:\n${JSON.stringify(rows, null, 2)}` : '',
summary ? `汇总:\n${JSON.stringify(summary, null, 2)}` : '',
].filter(Boolean).join('\n\n');
};
const summarizeKpiSnapshot = snapshot => {
const metrics = Array.isArray(snapshot?.metrics) ? snapshot.metrics : [];
if (!metrics.length) return '';
return `KPI 指标:\n${JSON.stringify(metrics, null, 2)}`;
};
const summarizeBlockSnapshot = (block = {}) => {
const blockType = String(block?.blockType || '').trim().toLowerCase();
const snapshot = block?.contentSnapshot || {};
if (blockType === 'chart') return summarizeChartSnapshot(snapshot);
if (blockType === 'table') return summarizeTableSnapshot(snapshot);
if (blockType === 'kpi') return summarizeKpiSnapshot(snapshot);
return normalizeOptionalString(snapshot?.content) || '';
};
const analyzeBlockSnapshotWithPrompt = async (ctx, options = {}) => {
const prompt = normalizeOptionalString(options?.prompt);
const summary = summarizeBlockSnapshot(options?.sourceBlock || {});
const blockType = normalizeOptionalString(options?.sourceBlock?.blockType) || 'block';
if (!prompt) throw '缺少参数: prompt';
if (!summary) throw 'AI 分析缺少可用的数据块快照';
const actionId = options.actionId || `report-block:${uuidv4()}`;
const content = await requestFastGptTextWithApp(
ctx,
[
`请基于以下${blockType}数据快照完成分析。`,
'如果数据不足,请明确说明。',
`分析要求: ${prompt}`,
'',
summary,
].join('\n'),
{
actionId,
userId: options.userId,
variables: {
type: 'ai分析',
blockType,
},
}
);
await reportBusinessCall({
ctx,
applicationId: 'exp-report',
eventId: actionId,
traceId: actionId,
userId: options.userId,
reportContext: { action: 'block_analysis', blockId: options?.sourceBlock?.id },
});
return content;
};
module.exports.analyzeReportBlock = async (ctx, next) => {
try {
const { prompt, image } = ctx.request.body || {};
const normalizedPrompt = String(prompt || '').trim();
const normalizedImage = String(image || '').trim();
if (!normalizedPrompt) throw '缺少参数: prompt';
if (!normalizedImage) throw '缺少参数: image';
const config = ctx.app.fs.config.fastGpt || {};
if (!config.apiUrl) throw '未配置 fastGpt.apiUrl';
const key = getFastGptKey(ctx.app);
if (!key) throw '未配置 fastGpt.reportAppKey';
const res = await superagent
.post(`${config.apiUrl}/api/v1/chat/completions`)
.send({
stream: false,
detail: true,
variables: {
type: 'ai分析',
},
messages: [
{
role: 'user',
content: [
{
type: 'image_url',
image_url: {
url: normalizedImage,
},
},
{
type: 'text',
text: normalizedPrompt,
},
],
},
],
})
.set({
Authorization: `Bearer ${key}`,
'Content-Type': 'application/json',
});
const actionId = `report-image:${uuidv4()}`;
await reportFastgptResponse({
ctx,
applicationId: 'exp-report',
actionId,
responseBody: res?.body,
appKey: key,
includeCall: false,
});
const content = normalizeFastGptMessageContent(
res?.body?.choices?.[0]?.message?.content
);
if (!content) throw 'fastgpt 返回内容为空';
await reportBusinessCall({
ctx,
applicationId: 'exp-report',
eventId: actionId,
traceId: actionId,
reportContext: { action: 'image_analysis' },
});
ctx.body = {
content,
};
ctx.status = 200;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error === 'string' ? error : 'AI 分析失败',
};
}
};
module.exports = {
requestFastGptTextWithApp,
parseOutlineContent,
analyzeBlockSnapshotWithPrompt,
analyzeReportBlock: module.exports.analyzeReportBlock,
};