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.
 
 
 

180 lines
8.7 KiB

'use strict';
const crypto = require('crypto');
const superagent = require('superagent');
const { v4: uuidv4 } = require('uuid');
const { collectResponseDataNodes, unwrapResponseData } = require('../utils/fastgptResponseData');
const stableStringify = (value) => {
if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
if (value && typeof value === 'object') return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
return JSON.stringify(value);
};
const getNumber = (value) => Math.max(0, Number.parseInt(value, 10) || 0);
const getText = (value, max = 128) => String(value ?? '').trim().slice(0, max);
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
const resolveApiKeyId = (config, appKey) => {
const value = String(appKey || '').trim();
if (!value) return '';
try {
const mapping = typeof config?.analytics?.fastgptKeyIds === 'string'
? JSON.parse(config.analytics.fastgptKeyIds)
: config?.analytics?.fastgptKeyIds || {};
return getText(mapping[sha256(value)]);
} catch (error) {
return '';
}
};
const resolveContextUserId = (ctx, fallbackUserId = '') => {
const user = ctx?.fs?.curUser?.userInfo || {};
return getText(
fallbackUserId || ctx?.fs?.userIdMapping?.internalUserId || user.id ||
user.pepUserId || user.pepId || ctx?.request?.body?.userId ||
ctx?.request?.body?.creator || ctx?.request?.query?.userId,
64,
);
};
const buildCallCompletedEvent = ({ eventId, applicationId, userId, traceId, occurredAt, durationMs = 0, apiKeyId = '', dataQuality = 'server_action' }) => ({
eventId: getText(eventId),
eventType: 'call_completed',
status: 'success',
occurredAt: occurredAt || new Date().toISOString(),
traceId: getText(traceId || eventId),
applicationId: getText(applicationId),
userId: getText(userId, 64),
apiKeyId: getText(apiKeyId),
durationMs: getNumber(durationMs),
dataQuality,
});
const getEventSuffix = (value) => sha256(value).slice(0, 20);
const buildFastgptEvents = ({ responseData, applicationId, userId, traceId, invocationId, occurredAt, apiKeyId = '' }) => {
const root = unwrapResponseData(responseData);
const requestId = getText(root.chatItemDataId || root.dataId || root.id, 128);
const effectiveInvocationId = getText(invocationId || traceId || uuidv4(), 128);
const effectiveTraceId = getText(traceId || requestId || effectiveInvocationId, 128);
const eventScope = getEventSuffix(stableStringify({
applicationId: getText(applicationId), userId: getText(userId, 64),
traceId: effectiveTraceId, requestId, invocationId: effectiveInvocationId,
}));
const rootEventId = `fastgpt:${eventScope}:call`;
const events = [{
eventId: rootEventId,
eventType: 'call_completed',
status: 'success',
occurredAt,
traceId: effectiveTraceId,
applicationId,
userId,
apiKeyId,
durationMs: getNumber(root.runningTime || root.durationMs),
dataQuality: 'fastgpt_detail',
}];
const nodes = collectResponseDataNodes(root);
const tokenNodes = nodes.filter((node) => node.kind === 'model' || node.kind === 'embedding');
tokenNodes.forEach((node, index) => {
const nodeSuffix = getEventSuffix(`${index}|${node.path}|${node.nodeId}|${node.model}`);
events.push({
eventId: `fastgpt:${eventScope}:usage:${nodeSuffix}`, eventType: 'model_usage', status: 'success', occurredAt,
traceId: effectiveTraceId, nodeId: node.nodeId, applicationId, userId, model: node.model,
apiKeyId,
inputTokens: node.inputTokens, outputTokens: node.outputTokens, totalTokens: node.totalTokens, dataQuality: 'fastgpt_detail',
});
});
nodes.filter((node) => node.kind === 'rag' || node.ragQueryCount).forEach((node, index) => {
const nodeSuffix = getEventSuffix(`${index}|${node.path}|${node.nodeId}|${node.knowledgeBaseId}`);
events.push({
eventId: `fastgpt:${eventScope}:rag:${nodeSuffix}`, eventType: 'knowledge_retrieval', status: 'success', occurredAt,
traceId: effectiveTraceId, nodeId: node.nodeId, applicationId, userId,
apiKeyId,
knowledgeBaseId: node.knowledgeBaseId, knowledgeBaseName: node.knowledgeBaseName,
ragQueryCount: node.ragQueryCount, ragHitCount: node.ragHitCount, dataQuality: 'fastgpt_detail',
});
});
return events;
};
const formatReportContext = (context = {}) => Object.entries(context)
.filter(([, value]) => value !== undefined && value !== null && value !== '')
.map(([key, value]) => `${key}=${getText(value, 128)}`)
.join(' ');
const reportUsage = async (conf, events, logger, reportContext = {}) => {
const centerUrl = String(conf?.analytics?.centerUrl || process.env.AI_CENTER_URL || '').replace(/\/$/, '');
const keyId = String(conf?.analytics?.keyId || process.env.ANALYTICS_HMAC_KEY_ID || '').trim();
const secret = String(conf?.analytics?.secret || process.env.ANALYTICS_HMAC_SECRET || '').trim();
const context = formatReportContext(reportContext);
if (!events.length) return false;
if (!centerUrl || !keyId || !secret) {
logger?.warn?.(`[dashboardReporter] report skipped: missing analytics configuration ${context}`.trim());
return false;
}
const body = { events };
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = uuidv4();
const bodyHash = sha256(stableStringify(body));
const canonical = `${timestamp}\n${nonce}\nPOST\n/analytics/token-usage/report\n${bodyHash}`;
const signature = crypto.createHmac('sha256', secret).update(canonical).digest('hex');
try {
await superagent.post(`${centerUrl}/analytics/token-usage/report`).set({
'Content-Type': 'application/json', 'X-Analytics-Key-Id': keyId,
'X-Analytics-Timestamp': timestamp, 'X-Analytics-Nonce': nonce, 'X-Analytics-Signature': signature,
}).send(body).timeout(10000);
return true;
} catch (error) {
logger?.warn?.(`[dashboardReporter] report failed: ${error.message} ${context}`.trim());
return false;
}
};
const reportBusinessCall = async ({ ctx, applicationId, eventId, userId, traceId, occurredAt, durationMs = 0, reportContext = {} }) => {
const resolvedUserId = resolveContextUserId(ctx, userId);
if (!resolvedUserId || !applicationId || !eventId) {
ctx?.logger?.warn?.(`[dashboardReporter] business call skipped: missing ${!resolvedUserId ? 'userId' : !applicationId ? 'applicationId' : 'eventId'} applicationId=${getText(applicationId)} ${formatReportContext(reportContext)}`.trim());
return false;
}
return reportUsage(ctx?.app?.fs?.config, [buildCallCompletedEvent({
eventId, applicationId, userId: resolvedUserId, traceId, occurredAt, durationMs,
})], ctx?.logger, reportContext);
};
const reportFastgptResponse = async ({ ctx, applicationId, actionId, responseBody, appKey, userId, includeCall = true, occurredAt, reportContext = {} }) => {
const resolvedUserId = resolveContextUserId(ctx, userId);
if (!resolvedUserId || !applicationId || !actionId || !responseBody) {
ctx?.logger?.warn?.(`[dashboardReporter] FastGPT report skipped: missing ${!resolvedUserId ? 'userId' : !applicationId ? 'applicationId' : !actionId ? 'actionId' : 'responseBody'} applicationId=${getText(applicationId)} ${formatReportContext(reportContext)}`.trim());
return false;
}
const events = buildFastgptEvents({
responseData: responseBody?.responseData || responseBody,
applicationId,
userId: resolvedUserId,
traceId: actionId,
invocationId: actionId,
occurredAt: occurredAt || new Date().toISOString(),
apiKeyId: resolveApiKeyId(ctx?.app?.fs?.config, appKey),
});
const reportEvents = includeCall ? events : events.filter((event) => event.eventType !== 'call_completed');
if (!reportEvents.some((event) => event.eventType === 'model_usage')) {
ctx?.logger?.warn?.(`[dashboardReporter] FastGPT response has no usage nodes applicationId=${getText(applicationId)} ${formatReportContext(reportContext)}`.trim());
}
return reportUsage(ctx?.app?.fs?.config, reportEvents, ctx?.logger, reportContext);
};
const dashboardReporterService = (app) => {
app.fs = app.fs || {};
app.fs.dashboardReporter = dashboardReporterService;
};
dashboardReporterService.buildFastgptEvents = buildFastgptEvents;
dashboardReporterService.buildCallCompletedEvent = buildCallCompletedEvent;
dashboardReporterService.reportUsage = reportUsage;
dashboardReporterService.reportBusinessCall = reportBusinessCall;
dashboardReporterService.reportFastgptResponse = reportFastgptResponse;
dashboardReporterService.resolveApiKeyId = resolveApiKeyId;
dashboardReporterService.resolveContextUserId = resolveContextUserId;
module.exports = dashboardReporterService;