106 lines
3.3 KiB
TypeScript
106 lines
3.3 KiB
TypeScript
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);
|
||
}
|
||
}
|