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
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,
|
|
};
|
|
|