Files
omniai-server/src/enterpriseVideoBilling.js
T

246 lines
7.5 KiB
JavaScript
Raw Normal View History

2026-06-02 13:14:10 +08:00
"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",
2026-06-02 13:14:10 +08:00
]);
const CREDITS_PER_CNY = 100;
const CREDIT_UNITS_PER_CREDIT = 100;
2026-06-02 13:14:10 +08:00
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 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;
2026-06-02 13:14:10 +08:00
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);
if (model.includes("happyhorse")) {
return resolution === "720P" ? 0.72 : 1.28;
}
if (model.includes("wan2.7-i2v") || model.includes("wanxiang")) {
return resolution === "720P" ? 0.6 : 1;
}
if (model.includes("animate-mix") || model.includes("s2v")) {
return resolution === "720P" ? 0.6 : 1;
}
if (model.includes("kling")) {
if (input.muted) {
if (input.hasReferenceVideo) return resolution === "720P" ? 0.9 : 1.2;
return resolution === "720P" ? 0.6 : 0.8;
}
return resolution === "720P" ? 0.9 : 1.2;
}
if (model.includes("vidu")) {
return resolution === "720P" ? 0.6 : 1.0;
}
if (model.includes("pixverse")) {
return resolution === "720P" ? 0.6 : 1.0;
}
2026-06-02 13:14:10 +08:00
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 calculateEnterpriseVideoCredits(input) {
const duration = normalizeEnterpriseVideoDuration(input.durationSeconds || input.duration);
return Number((getEnterpriseVideoCreditRate(input) * duration * CREDITS_PER_CNY).toFixed(2));
2026-06-02 13:14:10 +08:00
}
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);
2026-06-02 13:14:10 +08:00
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;
}
2026-06-02 13:14:10 +08:00
module.exports = {
ENTERPRISE_VIDEO_ALLOWED_MODELS,
assertEnterpriseVideoModelAllowed,
calculateEnterpriseVideoCost,
calculateEnterpriseVideoCredits,
getEnterpriseVideoCreditRate,
isEnterpriseVideoBillingUser,
isEnterpriseVideoModelAllowed,
markEnterpriseVideoCreditsAccepted,
normalizeEnterpriseVideoDuration,
normalizeEnterpriseVideoResolution,
prepareEnterpriseVideoBilling,
refundEnterpriseVideoCredits,
2026-06-02 13:14:10 +08:00
reserveEnterpriseVideoCredits,
};