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.
 
 
 

139 lines
4.4 KiB

'use strict';
const moment = require('moment');
const { reportFastgptResponse } = require('../services/dashboardReporter');
// 从运行时上下文中获取额度/计费服务实例。
const getQuotaService = (ctx) => ctx?.app?.fs?.quotaBillingService;
// 记录一次生成请求的 token 使用量,并按规则完成免费判定与扣费结算。
const trackGenerateUsage = async (ctx, {
creator,
docKind,
docId,
chapterKey = null,
responseBody,
analyticsApplicationId = '',
analyticsAppKey = '',
ignoreCreateFree = false,
allowChapterFirstFree = true,
}) => {
try {
const quotaService = getQuotaService(ctx);
if (!quotaService || !docKind) return;
const userId = quotaService.normalizeUserId(creator);
const normalizedDocId = Number(docId);
if (!userId || !Number.isFinite(normalizedDocId) || normalizedDocId <= 0) return;
const usage = quotaService.toUsageFromResponse(responseBody || {});
let billingStatus = quotaService.BILLING_STATUS.SKIP;
let amountYuan = 0;
let requestId = null;
const docIsCreateFree = ignoreCreateFree
? false
: await quotaService.isDocGenerateBillingExemptByCreate({
userId,
docId: normalizedDocId,
docKind,
});
// 仅在存在有效 token 消耗时进入计费流程。
if (usage.totalTokens > 0 && !docIsCreateFree) {
let isFree = false;
// 章节级首免:首次生成不扣费,仅记录免费状态。
if (chapterKey && allowChapterFirstFree) {
isFree = await quotaService.markChapterFirstFree({
userId,
docId: normalizedDocId,
chapterKey: String(chapterKey),
});
}
if (isFree) {
billingStatus = quotaService.BILLING_STATUS.FREE;
} else {
billingStatus = quotaService.BILLING_STATUS.PENDING;
amountYuan = quotaService.calculateAmountYuan(usage.totalTokens);
requestId = quotaService.createRequestId({
userId,
docKind,
docId: normalizedDocId,
chapterKey: chapterKey ? String(chapterKey) : '',
});
}
}
const eventId = await quotaService.recordGenerateEvent({
userId,
docId: normalizedDocId,
docKind,
chapterKey: chapterKey ? String(chapterKey) : null,
usage,
billingStatus,
amountYuan,
requestId,
});
// 仅对待结算记录执行实际扣费,避免重复计费。
if (billingStatus === quotaService.BILLING_STATUS.PENDING && eventId) {
const aideductUserId = await quotaService.resolveChargeUserId({
userId,
externalUserId: ctx?.fs?.userIdMapping?.externalUserId || '',
});
await quotaService.settleGenerateBilling({
eventId,
aideductUserId,
amountYuan,
requestId,
logger: ctx.logger,
});
}
if (analyticsApplicationId && responseBody) {
await reportFastgptResponse({
ctx,
applicationId: analyticsApplicationId,
actionId: `quota:${docKind}:${normalizedDocId}:${chapterKey || eventId || Date.now()}`,
responseBody,
appKey: analyticsAppKey,
userId,
// Full-document generation reports one business call after all
// chapters succeed; individual chapter responses only carry usage.
includeCall: !chapterKey,
});
}
} catch (error) {
ctx?.logger?.log?.(error);
}
};
// 写入 AI 查询日志(用于行为追踪与审计),支持复用外部事务。
const recordAiQueryLog = async (ctx, {
userId,
ipAddress,
feature,
clientId = 'pep-feixiaoshang',
time = null,
transaction = null,
}) => {
try {
const models = ctx?.app?.fs?.dc?.models;
if (!models?.AiQueryRecord || !userId || !feature) return;
const createOptions = transaction ? { transaction } : undefined;
const payload = {
userId,
ipAddress,
clientId,
feature,
time: time || moment(),
};
await models.AiQueryRecord.create(payload, createOptions);
} catch (error) {
ctx?.logger?.log?.(error);
}
};
module.exports = {
getQuotaService,
trackGenerateUsage,
recordAiQueryLog,
};