一键成片优化

This commit is contained in:
Azmat@qq.com
2026-09-04 18:16:43 +08:00
parent ee3ddb49b4
commit 0d042edc73
6 changed files with 130 additions and 7 deletions
+46 -4
View File
@@ -29,6 +29,7 @@ import {
estimateCost,
FC_MODELS,
modelLabel,
pointsPerCallFromCatalog,
pointsPerImageFromCatalog,
type BillingRates,
} from "../components/free-create/constants";
@@ -60,6 +61,10 @@ const QUICK_RESOLUTIONS = [
{ value: "4k", label: "4K 超清" },
];
const QUICK_DURATIONS = [15, 30, 45, 60];
/** 与后端 QUICK_SCRIPT_MODEL_NAME 一致 */
const QUICK_SCRIPT_MODEL = "doubao-seed-2-1-pro-260628";
const QUICK_SCRIPT_POINTS_FALLBACK = 100;
const QUICK_IMAGE_POINTS_FALLBACK = 20;
function modelResolutions(config: ModelConfig | undefined) {
const capabilities = (config?.metadata?.capabilities || {}) as Record<string, unknown>;
@@ -413,6 +418,24 @@ export function QuickCreatePage({
onNotify?.("info", "请选择商品品类,脚本会按品类来写");
return;
}
// 与后端门禁同口径:可用积分不够预估总额就不提交。
if (estimatedPoints > 0) {
try {
const summary = await api.billingSummary();
const balance = Number(summary.account?.balance || 0);
const reserved = Number(summary.account?.reserved_balance || 0);
const available = balance - reserved;
if (available < estimatedPoints) {
onNotify?.(
"info",
`积分不足:预计需要 ${estimatedPoints} 积分,当前可用 ${Math.max(0, Math.floor(available))} 积分,请充值后再开始`,
);
return;
}
} catch {
/* 余额接口失败时仍交给后端硬拦 */
}
}
setSubmitting(true);
setJob(null);
setServiceUnavailable(false);
@@ -465,6 +488,8 @@ export function QuickCreatePage({
setServiceUnavailable(true);
setUnavailableMessage(error.message || "一键成片服务暂不可用,请稍后再试");
onNotify?.("info", error.message || "一键成片服务暂不可用,请稍后再试");
} else if (error instanceof ApiError && error.status === 400 && /余额不足|积分不足/.test(error.message || "")) {
onNotify?.("info", error.message);
} else {
onNotify?.("error", error instanceof Error ? error.message : "一键成片启动失败");
}
@@ -572,14 +597,31 @@ export function QuickCreatePage({
{ ratio: aspectRatio, resolution, duration: totalDuration, refs: [] },
billingRates,
);
// 基础资产按图像挂牌预估;视频挂牌秒价。最终按成功任务实扣。
// 与实扣对齐:脚本(Seed 2.1 Pro 挂牌) + 默认出图挂牌×场次粗估 + 视频挂牌秒价×时长
const applyTeam = (raw: number) => {
const multiplier = billingRates.multiplier || 1;
if (!(raw > 0)) return 0;
return multiplier === 1 ? raw : Math.max(1, Math.round(Number((raw * multiplier).toFixed(6))));
};
const scriptUnit = (() => {
const texts = modelConfigs.filter((m) => m.capability === "text" && m.status === "active");
const script = texts.find((m) => m.name === QUICK_SCRIPT_MODEL) || texts.find((m) => m.is_default) || texts[0];
const listed = pointsPerCallFromCatalog(script);
return listed != null && listed > 0 ? listed : QUICK_SCRIPT_POINTS_FALLBACK;
})();
const imageUnit = (() => {
const img = modelConfigs.find((m) => m.capability === "image" && m.status === "active")
const images = modelConfigs.filter((m) => m.capability === "image" && m.status === "active");
// 与后端 get_default_model(IMAGE) 一致:优先 is_default,再回落影擎-Image2 名字
const img = images.find((m) => m.is_default)
|| images.find((m) => m.name === "gpt-image-2")
|| images[0]
|| modelConfigs.find((m) => m.capability === "image");
const listed = pointsPerImageFromCatalog(img);
return listed != null && listed > 0 ? listed : 20;
return listed != null && listed > 0 ? listed : QUICK_IMAGE_POINTS_FALLBACK;
})();
const estimatedPoints = videoEstimate.listed ? videoEstimate.points + sceneCount * imageUnit : 0;
const estimatedPoints = videoEstimate.listed
? videoEstimate.points + applyTeam(scriptUnit) + applyTeam(sceneCount * imageUnit)
: 0;
const shellClass = [
"quick-create-shell",
restoring ? "is-restoring" : "",
+3 -2
View File
@@ -9,7 +9,8 @@ import {
UserRound,
Users,
Wallet,
WandSparkles
WandSparkles,
type LucideIcon,
} from "lucide-react";
export type Page =
@@ -79,7 +80,7 @@ export type NavigateOptions = {
};
export type NavigateFn = (page: Page, options?: NavigateOptions) => void;
export type Notice = { type: "success" | "error" | "info"; text: string } | null;
export type NavItem = { page: Page; label: string; icon: typeof Home; badge?: string };
export type NavItem = { page: Page; label: string; icon: LucideIcon; badge?: string };
// 仅团队主账号可访问的团队级页面。入口与路由守卫共用此规则,避免权限逻辑分散。
const OWNER_ONLY_PAGES = new Set<Page>(["team", "account"]);
+1
View File
@@ -588,6 +588,7 @@ export type ModelConfig = {
display_name: string;
capability: string;
status: string;
is_default?: boolean;
unit_price?: string; // 单位价(每张图/每次调用),前端据此算「预估扣费」与后端实扣一致(PMC#20)
// 模型元数据:自由创作视频模型带 pricing(元/百万tokens 分档价表)/resolutions/durations,前端预估消耗读它
metadata?: Record<string, unknown>;