2026-05-27 09:43:43 +08:00

106 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createLogger } from "@/lib/observability/logger";
import { getModelGenerationCreditCost } from "@/lib/ai/image-model-pricing";
import {
consumeUserPoints,
getAiModelById,
getEnabledAiModelsWithProvider,
getPointsSystemConfig,
getUserById,
} from "@/lib/db/queries";
const chatApiLogger = createLogger("chat.billing");
const MODEL_UUID_REGEX =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
export function getModelCreditCost(
modelConfig: any,
target: "image" | "video",
options?: {
duration?: number | null;
imageSize?: string | null;
resolution?: string | null;
},
): number {
return getModelGenerationCreditCost(modelConfig, target, options);
}
export async function resolveBillingModelByReference(modelRef: string): Promise<any | undefined> {
if (!modelRef) return undefined;
if (MODEL_UUID_REGEX.test(modelRef)) {
return await getAiModelById({ id: modelRef }).catch(() => undefined);
}
const allModels = await getEnabledAiModelsWithProvider(true, true).catch(() => []);
const matched = allModels.find(
({ model }: any) => model.id === modelRef || model.modelId === modelRef,
);
return matched?.model;
}
export async function consumeGeneratedVideoPoints(params: {
userId: string;
chatId: string;
messageId: string;
modelRefs: Array<string | null | undefined>;
duration?: number | null;
resolution?: string | null;
}) {
try {
const pointsConfig = await getPointsSystemConfig();
if (!pointsConfig.enabled) return;
const userData = await getUserById({ id: params.userId }).catch(() => undefined);
if (userData?.isHighVip) {
chatApiLogger.info(`[Points] Skip video points for VIP user=${params.userId}`);
return;
}
let billingModel: any | undefined;
for (const ref of params.modelRefs) {
if (!ref) continue;
const resolved = await resolveBillingModelByReference(ref);
if (!resolved) continue;
billingModel = resolved;
if (
getModelCreditCost(resolved, "video", {
duration: params.duration,
resolution: params.resolution,
}) > 0
) {
break;
}
}
if (!billingModel) return;
const unitCost = getModelCreditCost(billingModel, "video", {
duration: params.duration,
resolution: params.resolution,
});
if (unitCost <= 0) return;
const consumeResult = await consumeUserPoints({
userId: params.userId,
amount: unitCost,
reason: "video_generation",
modelId: billingModel.id,
chatId: params.chatId,
messageId: params.messageId,
note: `模型(${billingModel.displayName || billingModel.modelId || billingModel.id}) 生视频 1 条${params.duration ? `(${params.duration} 秒)` : ""}${params.resolution ? `(${params.resolution})` : ""}`,
});
if (!consumeResult.ok) {
chatApiLogger.warn(`[Points] Failed to consume video points. user=${params.userId} model=${billingModel.id} cost=${unitCost}`);
} else if (consumeResult.skipped) {
chatApiLogger.info(`[Points] Skipped ${unitCost} video points for VIP user=${params.userId} model=${billingModel.id}`);
} else {
chatApiLogger.info(`[Points] Consumed ${unitCost} points for video. balance=${consumeResult.balance}`);
}
} catch (error) {
chatApiLogger.error("[Points] Failed to consume points after video generation:", error);
}
}