"use strict"; const ENTERPRISE_VIDEO_ALLOWED_MODELS = new Set([ "happyhorse-1.0", "happyhorse-1.0-t2v", "happyhorse-1.0-i2v", "happyhorse-1.0-r2v", "wan2.7-i2v", "wan2.2-animate-mix", "wan2.2-s2v", "kling-3.0-dashscope", "kling-v3-omni-dashscope", "kling/kling-v3-omni-video-generation", "vidu-q3-turbo", "vidu-q3-turbo-t2v", "vidu-q3-turbo-i2v", "pixverse-c1", "pixverse-c1-t2v", "pixverse-c1-i2v", ]); const CREDITS_PER_CNY = 100; const CREDIT_UNITS_PER_CREDIT = 100; const ENTERPRISE_VIDEO_RESOLUTIONS = ["720P", "1080P"]; const ENTERPRISE_VIDEO_DEFAULT_RESOLUTION = "1080P"; const ENTERPRISE_VIDEO_PRICING_RULES = [ { id: "happyhorse", modelIncludes: ["happyhorse"], rates: { "720P": 0.72, "1080P": 1.28 }, }, { id: "wanxiang-i2v", modelIncludes: ["wan2.7-i2v", "wanxiang"], rates: { "720P": 0.6, "1080P": 1 }, }, { id: "wan-animate-s2v", modelIncludes: ["animate-mix", "s2v"], rates: { "720P": 0.6, "1080P": 1 }, }, { id: "kling-muted-reference", modelIncludes: ["kling"], when: { muted: true, hasReferenceVideo: true }, rates: { "720P": 0.9, "1080P": 1.2 }, }, { id: "kling-muted", modelIncludes: ["kling"], when: { muted: true, hasReferenceVideo: false }, rates: { "720P": 0.6, "1080P": 0.8 }, }, { id: "kling-default", modelIncludes: ["kling"], rates: { "720P": 0.9, "1080P": 1.2 }, }, { id: "vidu", modelIncludes: ["vidu"], rates: { "720P": 0.6, "1080P": 1 }, }, { id: "pixverse", modelIncludes: ["pixverse"], rates: { "720P": 0.6, "1080P": 1 }, }, ]; function normalizeModel(value) { return String(value || "").trim().toLowerCase(); } function normalizeEnterpriseVideoResolution(value) { return String(value || "").trim().toUpperCase() === "720P" ? "720P" : "1080P"; } function normalizeEnterpriseVideoDuration(value) { const numeric = Number(value); if (!Number.isFinite(numeric)) return 1; return Math.max(1, Math.ceil(numeric)); } function enterpriseVideoPricingRuleMatches(rule, input, model) { if (!rule.modelIncludes.some((pattern) => model.includes(pattern))) return false; if (!rule.when) return true; if (Object.prototype.hasOwnProperty.call(rule.when, "muted") && Boolean(input.muted) !== rule.when.muted) { return false; } if ( Object.prototype.hasOwnProperty.call(rule.when, "hasReferenceVideo") && Boolean(input.hasReferenceVideo) !== rule.when.hasReferenceVideo ) { return false; } return true; } function isEnterpriseVideoBillingUser(user) { return Boolean(user?.enterpriseId); } function isEnterpriseVideoModelAllowed(providerConfig, model) { const requestedModel = normalizeModel(model || providerConfig?.requestedModel || providerConfig?.model); const resolvedModel = normalizeModel(providerConfig?.model); const protocol = String(providerConfig?.protocol || "").toLowerCase(); if (ENTERPRISE_VIDEO_ALLOWED_MODELS.has(requestedModel)) return true; if (ENTERPRISE_VIDEO_ALLOWED_MODELS.has(resolvedModel)) return true; if (protocol.startsWith("happyhorse-")) return true; if (protocol === "wan-i2v") return true; if (protocol === "wan-animate-mix") return true; if (protocol === "wan-s2v") return true; if (protocol === "kling-dashscope") return true; if (protocol === "vidu") return true; if (protocol === "pixverse") return true; return false; } function assertEnterpriseVideoModelAllowed(providerConfig, model) { if (isEnterpriseVideoModelAllowed(providerConfig, model)) return; const error = new Error("该企业账号不可使用当前视频模型"); error.status = 403; error.code = "ENTERPRISE_VIDEO_MODEL_NOT_ALLOWED"; throw error; } function getEnterpriseVideoCreditRate(input) { const resolution = normalizeEnterpriseVideoResolution(input.resolution || input.quality); const model = normalizeModel(input.model || input.requestedModel); const rule = ENTERPRISE_VIDEO_PRICING_RULES.find((candidate) => enterpriseVideoPricingRuleMatches(candidate, input, model), ); if (rule) return rule.rates[resolution] ?? rule.rates[ENTERPRISE_VIDEO_DEFAULT_RESOLUTION]; const error = new Error(`Unsupported enterprise video model: ${input.model || input.requestedModel}`); error.status = 403; error.code = "ENTERPRISE_VIDEO_MODEL_NOT_ALLOWED"; throw error; } function getEnterpriseVideoPricingConfig() { return { currency: "CNY", creditsPerCny: CREDITS_PER_CNY, billingUnit: "per_second", defaultResolution: ENTERPRISE_VIDEO_DEFAULT_RESOLUTION, resolutions: [...ENTERPRISE_VIDEO_RESOLUTIONS], rules: ENTERPRISE_VIDEO_PRICING_RULES.map((rule) => ({ id: rule.id, modelIncludes: [...rule.modelIncludes], when: rule.when ? { ...rule.when } : undefined, rates: { ...rule.rates }, })), }; } function calculateEnterpriseVideoCredits(input) { const duration = normalizeEnterpriseVideoDuration(input.durationSeconds || input.duration); return Number((getEnterpriseVideoCreditRate(input) * duration * CREDITS_PER_CNY).toFixed(2)); } function calculateEnterpriseVideoCost(input) { const resolution = normalizeEnterpriseVideoResolution(input.resolution || input.quality); const durationSeconds = normalizeEnterpriseVideoDuration(input.durationSeconds || input.duration); const rateCreditsPerSecond = getEnterpriseVideoCreditRate({ ...input, resolution, durationSeconds, }); const rateCentsPerSecond = Math.round(rateCreditsPerSecond * CREDITS_PER_CNY * CREDIT_UNITS_PER_CREDIT); return { resolution, durationSeconds, rateCentsPerSecond, amountCents: rateCentsPerSecond * durationSeconds, }; } function prepareEnterpriseVideoBilling({ user, providerConfig, params }) { if (!isEnterpriseVideoBillingUser(user)) return null; assertEnterpriseVideoModelAllowed(providerConfig, params.requestedModel || params.model); const hasReferenceVideo = Boolean(params.hasReferenceVideo); const muted = Boolean(params.muted); const pricing = calculateEnterpriseVideoCost({ model: params.model, requestedModel: params.requestedModel, resolution: params.resolution || params.quality, duration: params.duration, muted, hasReferenceVideo, }); return { enterpriseId: user.enterpriseId, userId: user.id, model: params.model, requestedModel: params.requestedModel, muted, hasReferenceVideo, ...pricing, }; } async function reserveEnterpriseVideoCredits(client, billing) { if (!billing || billing.amountCents <= 0) return null; const { rows: balanceRows } = await client.query( "UPDATE enterprises SET balance_cents = balance_cents - $1, updated_at = NOW() WHERE id = $2 AND enabled = 1 AND balance_cents >= $1 RETURNING balance_cents", [billing.amountCents, billing.enterpriseId], ); if (balanceRows.length === 0) { const error = new Error( `企业积分不足,预计需要 ${Number((billing.amountCents / 100).toFixed(2))} 积分`, ); error.status = 402; error.code = "INSUFFICIENT_ENTERPRISE_BALANCE"; throw error; } const { rows: [ledger], } = await client.query( ` INSERT INTO credit_ledger ( enterprise_id, user_id, task_id, model, task_type, resolution, duration_seconds, rate_cents_per_second, amount_cents, status ) VALUES ($1, $2, $3, $4, 'video', $5, $6, $7, $8, 'reserved') RETURNING id `, [ billing.enterpriseId, billing.userId, billing.taskId || null, billing.requestedModel || billing.model, billing.resolution, billing.durationSeconds, billing.rateCentsPerSecond, billing.amountCents, ], ); return { ...billing, creditLedgerId: ledger.id, enterpriseBalanceCents: Number(balanceRows[0].balance_cents || 0), }; } async function markEnterpriseVideoCreditsAccepted(clientOrPool, creditLedgerId) { if (!creditLedgerId) return false; const { rowCount } = await clientOrPool.query( "UPDATE credit_ledger SET status = 'charged', updated_at = NOW() WHERE id = $1 AND status = 'reserved'", [creditLedgerId], ); return rowCount > 0; } async function refundEnterpriseVideoCredits(clientOrPool, billing, reason) { if (!billing || !billing.creditLedgerId) return false; const { rowCount } = await clientOrPool.query( "UPDATE credit_ledger SET status = 'refunded', refund_reason = $1, updated_at = NOW() WHERE id = $2 AND status = 'reserved'", [reason || null, billing.creditLedgerId], ); if (rowCount > 0 && billing.amountCents > 0 && billing.enterpriseId) { await clientOrPool.query( "UPDATE enterprises SET balance_cents = balance_cents + $1, updated_at = NOW() WHERE id = $2", [billing.amountCents, billing.enterpriseId], ); } return rowCount > 0; } module.exports = { ENTERPRISE_VIDEO_ALLOWED_MODELS, assertEnterpriseVideoModelAllowed, calculateEnterpriseVideoCost, calculateEnterpriseVideoCredits, getEnterpriseVideoPricingConfig, getEnterpriseVideoCreditRate, isEnterpriseVideoBillingUser, isEnterpriseVideoModelAllowed, markEnterpriseVideoCreditsAccepted, normalizeEnterpriseVideoDuration, normalizeEnterpriseVideoResolution, prepareEnterpriseVideoBilling, refundEnterpriseVideoCredits, reserveEnterpriseVideoCredits, };