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