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