290 lines
12 KiB
TypeScript
290 lines
12 KiB
TypeScript
import {
|
||
createBillingPriceRule,
|
||
listBillingPriceRules,
|
||
updateBillingPriceRule,
|
||
type BillingPriceRuleInput
|
||
} from "@/lib/server/billing-store";
|
||
import type { BillingPriceRule } from "@/lib/types";
|
||
|
||
export const BILLING_USD_CNY_RATE = 7.2;
|
||
export const DEFAULT_BILLING_MARKUP_MULTIPLIER = 1.2;
|
||
export const BILLING_CATALOG_OBSERVED_AT = "2026-08-11";
|
||
|
||
const BAILIAN_IMAGE_URL = "https://help.aliyun.com/zh/model-studio/wan2-7-image-pro";
|
||
const BAILIAN_VIDEO_URL = "https://help.aliyun.com/zh/model-studio/wan2-7-i2v";
|
||
const EVOLINK_IMAGE_URL = "https://evolink.ai/zh/gpt-image-2";
|
||
const VOLC_ARK_PRICING_URL = "https://www.volcengine.com/docs/82379/1544106?lang=zh";
|
||
const JIMENG_PRICING_URL = "https://www.volcengine.com/activity/jimeng";
|
||
|
||
const EVOLINK_GPT_IMAGE_DIMENSIONS = [
|
||
{
|
||
key: "quality",
|
||
label: "生成质量",
|
||
baselineValue: "medium",
|
||
defaultValue: "medium",
|
||
tiers: [
|
||
{ value: "low", label: "快速", standardFactor: 0.11, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true, note: "官方口径约为 medium 的 0.11 倍成本。" },
|
||
{ value: "medium", label: "标准", standardFactor: 1, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true },
|
||
{ value: "high", label: "精细", standardFactor: 4, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true, note: "官方口径约为 medium 的 4 倍成本。" }
|
||
]
|
||
},
|
||
{
|
||
key: "resolution",
|
||
label: "分辨率",
|
||
baselineValue: "1K",
|
||
defaultValue: "1K",
|
||
tiers: [
|
||
{ value: "1K", label: "1K", standardFactor: 1, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true },
|
||
{ value: "2K", label: "2K", standardFactor: 4, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true, note: "按像素预算相对 1K 的估算倍率。" },
|
||
{ value: "4K", label: "4K", standardFactor: 8, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true, note: "按平台支持的最大像素预算估算。" }
|
||
]
|
||
},
|
||
{
|
||
key: "aspectRatio",
|
||
label: "画面比例",
|
||
baselineValue: "1:1",
|
||
defaultValue: "1:1",
|
||
tiers: [
|
||
{ value: "1:1", label: "1:1", standardFactor: 1, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true },
|
||
{ value: "4:3", label: "4:3", standardFactor: 1, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true },
|
||
{ value: "16:9", label: "16:9", standardFactor: 1, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true },
|
||
{ value: "9:16", label: "9:16", standardFactor: 1, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true },
|
||
{ value: "other", label: "其他比例", match: {}, standardFactor: 1, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true, note: "同一分辨率档位下按相同比例预算估算。" }
|
||
]
|
||
},
|
||
{
|
||
key: "referenceImageCount",
|
||
label: "参考图数量",
|
||
baselineValue: 0,
|
||
defaultValue: 0,
|
||
tiers: [
|
||
{ value: 0, label: "无参考图", standardFactor: 1, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true },
|
||
{ value: "1–4", label: "1–4 张", match: { min: 1, max: 4 }, standardFactor: 1, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true, note: "EvoLink 上游按 image input token 计费,当前平台按基础生成成本估算。" },
|
||
{ value: "5–8", label: "5–8 张", match: { min: 5, max: 8 }, standardFactor: 1, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true, note: "EvoLink 上游按 image input token 计费,当前平台按基础生成成本估算。" },
|
||
{ value: "9–16", label: "9–16 张", match: { min: 9, max: 16 }, standardFactor: 1, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true, note: "EvoLink 上游按 image input token 计费,当前平台按基础生成成本估算。" }
|
||
]
|
||
}
|
||
] satisfies NonNullable<BillingPriceRuleInput["parameterDimensions"]>;
|
||
|
||
export const DEFAULT_BILLING_PRICE_RULES: BillingPriceRuleInput[] = [
|
||
{
|
||
id: "base-volcengine-jimeng-seedream46",
|
||
provider: "volcengine-visual",
|
||
capability: "image.generate",
|
||
reqKey: "jimeng_seedream46_cvtob",
|
||
unit: "image",
|
||
standardUnitPriceFen: 20,
|
||
markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER,
|
||
enabled: true,
|
||
note: "即梦4.6参考基准:公开活动页按 ¥200/1000 张折算;官方实时价格以供应商控制台为准。",
|
||
source: {
|
||
url: JIMENG_PRICING_URL,
|
||
currency: "CNY",
|
||
unitPrice: 0.2,
|
||
basis: "公开资源包 ¥200/1000 张折算;非 API 实时刊例价",
|
||
observedAt: BILLING_CATALOG_OBSERVED_AT
|
||
}
|
||
},
|
||
{
|
||
id: "base-evolink-gpt-image-2",
|
||
provider: "evolink",
|
||
capability: "image.generate",
|
||
reqKey: "gpt-image-2",
|
||
unit: "image",
|
||
standardUnitPriceFen: 34,
|
||
markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER,
|
||
enabled: true,
|
||
note: "EvoLink 按 token 计费;当前以 medium / 1K / 1:1 / 无参考图作为平台标准基准,参数档位另列。",
|
||
parameterDimensions: EVOLINK_GPT_IMAGE_DIMENSIONS,
|
||
source: {
|
||
url: EVOLINK_IMAGE_URL,
|
||
currency: "USD",
|
||
unitPrice: 0.047,
|
||
fxRate: BILLING_USD_CNY_RATE,
|
||
basis: "页面估算:medium、1K、1:1、无参考图,约 $0.047/张;折算 ¥0.3384,账本按 ¥0.34",
|
||
observedAt: BILLING_CATALOG_OBSERVED_AT
|
||
}
|
||
},
|
||
{
|
||
id: "base-bailian-wan27-image-pro",
|
||
provider: "bailian",
|
||
capability: "image.generate",
|
||
reqKey: "wan2.7-image-pro",
|
||
unit: "image",
|
||
standardUnitPriceFen: 50,
|
||
markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER,
|
||
enabled: true,
|
||
note: "北京地域官方基础价:¥0.50/张。",
|
||
source: {
|
||
url: BAILIAN_IMAGE_URL,
|
||
currency: "CNY",
|
||
unitPrice: 0.5,
|
||
basis: "北京地域,按成功生成图片计费",
|
||
observedAt: BILLING_CATALOG_OBSERVED_AT
|
||
}
|
||
},
|
||
{
|
||
id: "base-bailian-wan27-i2v-720p",
|
||
provider: "bailian",
|
||
capability: "video.generate",
|
||
reqKey: "wan2.7-i2v-2026-04-25",
|
||
variantKey: "resolution=720p",
|
||
unit: "video_second",
|
||
standardUnitPriceFen: 60,
|
||
markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER,
|
||
enabled: true,
|
||
note: "北京地域官方基础价:720P ¥0.60/秒。",
|
||
source: {
|
||
url: BAILIAN_VIDEO_URL,
|
||
currency: "CNY",
|
||
unitPrice: 0.6,
|
||
basis: "北京地域,720P,按成功视频秒数计费",
|
||
observedAt: BILLING_CATALOG_OBSERVED_AT
|
||
}
|
||
},
|
||
{
|
||
id: "base-bailian-wan27-i2v-1080p",
|
||
provider: "bailian",
|
||
capability: "video.generate",
|
||
reqKey: "wan2.7-i2v-2026-04-25",
|
||
variantKey: "resolution=1080p",
|
||
unit: "video_second",
|
||
standardUnitPriceFen: 100,
|
||
markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER,
|
||
enabled: true,
|
||
note: "北京地域官方基础价:1080P ¥1.00/秒。",
|
||
source: {
|
||
url: BAILIAN_VIDEO_URL,
|
||
currency: "CNY",
|
||
unitPrice: 1,
|
||
basis: "北京地域,1080P,按成功视频秒数计费",
|
||
observedAt: BILLING_CATALOG_OBSERVED_AT
|
||
}
|
||
},
|
||
{
|
||
id: "base-seedance-2-0-480p",
|
||
provider: "seedance",
|
||
capability: "video.generate",
|
||
reqKey: "doubao-seedance-2-0-260128",
|
||
variantKey: "resolution=480p",
|
||
unit: "video_second",
|
||
standardUnitPriceFen: 46,
|
||
markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER,
|
||
enabled: true,
|
||
note: "官方 5 秒、16:9、无输入视频样例折算:¥2.31/5秒≈¥0.46/秒;实际按方舟 token 用量结算。",
|
||
source: {
|
||
url: VOLC_ARK_PRICING_URL,
|
||
currency: "CNY",
|
||
unitPrice: 0.46,
|
||
basis: "doubao-seedance-2.0,480P,无输入视频、16:9、5 秒示例折算;实际 token 计费可能随输入变化",
|
||
observedAt: BILLING_CATALOG_OBSERVED_AT
|
||
}
|
||
},
|
||
{
|
||
id: "base-seedance-2-0-720p",
|
||
provider: "seedance",
|
||
capability: "video.generate",
|
||
reqKey: "doubao-seedance-2-0-260128",
|
||
variantKey: "resolution=720p",
|
||
unit: "video_second",
|
||
standardUnitPriceFen: 99,
|
||
markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER,
|
||
enabled: true,
|
||
note: "官方 5 秒、16:9、无输入视频样例折算:¥4.97/5秒≈¥0.99/秒;实际按方舟 token 用量结算。",
|
||
source: {
|
||
url: VOLC_ARK_PRICING_URL,
|
||
currency: "CNY",
|
||
unitPrice: 0.99,
|
||
basis: "doubao-seedance-2.0,720P,无输入视频、16:9、5 秒示例折算;实际 token 计费可能随输入变化",
|
||
observedAt: BILLING_CATALOG_OBSERVED_AT
|
||
}
|
||
},
|
||
{
|
||
id: "base-seedance-2-0-1080p",
|
||
provider: "seedance",
|
||
capability: "video.generate",
|
||
reqKey: "doubao-seedance-2-0-260128",
|
||
variantKey: "resolution=1080p",
|
||
unit: "video_second",
|
||
standardUnitPriceFen: 248,
|
||
markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER,
|
||
enabled: true,
|
||
note: "官方 5 秒、16:9、无输入视频样例折算:¥12.39/5秒≈¥2.48/秒;实际按方舟 token 用量结算。",
|
||
source: {
|
||
url: VOLC_ARK_PRICING_URL,
|
||
currency: "CNY",
|
||
unitPrice: 2.48,
|
||
basis: "doubao-seedance-2.0,1080P,无输入视频、16:9、5 秒示例折算;实际 token 计费可能随输入变化",
|
||
observedAt: BILLING_CATALOG_OBSERVED_AT
|
||
}
|
||
},
|
||
{
|
||
id: "base-seedance-2-0-4k",
|
||
provider: "seedance",
|
||
capability: "video.generate",
|
||
reqKey: "doubao-seedance-2-0-260128",
|
||
variantKey: "resolution=4k",
|
||
unit: "video_second",
|
||
standardUnitPriceFen: 505,
|
||
markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER,
|
||
enabled: true,
|
||
note: "官方 5 秒、16:9、无输入视频样例折算:¥25.27/5秒≈¥5.05/秒;实际按方舟 token 用量结算。",
|
||
source: {
|
||
url: VOLC_ARK_PRICING_URL,
|
||
currency: "CNY",
|
||
unitPrice: 5.05,
|
||
basis: "doubao-seedance-2.0,4K,无输入视频、16:9、5 秒示例折算;实际 token 计费可能随输入变化",
|
||
observedAt: BILLING_CATALOG_OBSERVED_AT
|
||
}
|
||
}
|
||
];
|
||
|
||
export async function ensureDefaultBillingPriceRules(): Promise<BillingPriceRule[]> {
|
||
const existing = await listBillingPriceRules({ includeDisabled: true });
|
||
const created: BillingPriceRule[] = [];
|
||
for (const candidate of DEFAULT_BILLING_PRICE_RULES) {
|
||
const candidateKey = priceRuleMatchKey(candidate);
|
||
const existingRule = existing.find((rule) => priceRuleMatchKey(rule) === candidateKey);
|
||
if (existingRule) {
|
||
const updated = await updateBillingPriceRule(existingRule.id, {
|
||
reqKey: candidate.reqKey,
|
||
variantKey: candidate.variantKey,
|
||
unit: candidate.unit,
|
||
standardUnitPriceFen: candidate.standardUnitPriceFen,
|
||
enabled: candidate.enabled,
|
||
conditions: candidate.conditions,
|
||
quantitySource: candidate.quantitySource,
|
||
priority: candidate.priority,
|
||
note: candidate.note,
|
||
source: candidate.source,
|
||
parameterDimensions: candidate.parameterDimensions
|
||
});
|
||
if (updated) {
|
||
const index = existing.findIndex((rule) => rule.id === updated.id);
|
||
if (index >= 0) existing[index] = updated;
|
||
}
|
||
continue;
|
||
}
|
||
if (existing.some((rule) => rule.provider === candidate.provider
|
||
&& rule.capability === candidate.capability
|
||
&& !rule.reqKey
|
||
&& !rule.variantKey)) continue;
|
||
try {
|
||
const rule = await createBillingPriceRule(candidate);
|
||
created.push(rule);
|
||
} catch (error) {
|
||
if (!isConflict(error)) throw error;
|
||
}
|
||
}
|
||
return [...existing, ...created];
|
||
}
|
||
|
||
function priceRuleMatchKey(rule: Pick<BillingPriceRule, "provider" | "capability" | "reqKey" | "variantKey" | "conditions">): string {
|
||
const conditions = Object.fromEntries(Object.entries(rule.conditions || {}).sort(([left], [right]) => left.localeCompare(right)));
|
||
return [rule.provider, rule.capability, rule.reqKey || "", rule.variantKey || "", JSON.stringify(conditions)].join("\u0000");
|
||
}
|
||
|
||
function isConflict(error: unknown): boolean {
|
||
return typeof error === "object" && error !== null && "status" in error && (error as { status?: unknown }).status === 409;
|
||
}
|