diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 428cbe03b..928d6a5c6 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -5,6 +5,7 @@ import { useState, useEffect, useMemo, + useRef, type ChangeEvent, type DragEvent, } from "react"; @@ -1044,6 +1045,43 @@ const defaultLovartPromptTagVisibility: LovartPromptTagVisibility = { video: true, }; +function stableStringifyComparableValue(value: unknown): string { + if (value === null || typeof value !== "object") { + return JSON.stringify(value) ?? "null"; + } + + if (Array.isArray(value)) { + return `[${value.map((item) => stableStringifyComparableValue(item)).join(",")}]`; + } + + const entries = Object.entries(value as Record) + .filter(([, item]) => item !== undefined) + .sort(([left], [right]) => left.localeCompare(right)); + + return `{${entries + .map(([key, item]) => `${JSON.stringify(key)}:${stableStringifyComparableValue(item)}`) + .join(",")}}`; +} + +function areComparableValuesEqual(left: unknown, right: unknown) { + return stableStringifyComparableValue(left) === stableStringifyComparableValue(right); +} + +function pickChangedTopLevelFields>( + current: T, + baseline: Partial | null | undefined +) { + if (!baseline) return { ...current }; + + const changed: Partial = {}; + for (const [key, value] of Object.entries(current) as Array<[keyof T, T[keyof T]]>) { + if (!areComparableValuesEqual(value, baseline[key])) { + changed[key] = value; + } + } + return changed; +} + const defaultContactConfig: ContactConfig = { phone: "", wechatQrUrl: "", @@ -1588,6 +1626,10 @@ export function AdminSettingsClient() { htmlContent: "", updatedAt: null, }); + const lastLoadedSiteSettingsRef = useRef(null); + const lastLoadedHomeAnimationConfigRef = useRef( + null + ); const [newOverrideUserId, setNewOverrideUserId] = useState(""); const [newOverrideEmail, setNewOverrideEmail] = useState(""); const [newOverridePlanId, setNewOverridePlanId] = useState(""); @@ -1614,7 +1656,7 @@ export function AdminSettingsClient() { (siteSettingsData as any).dulupayConfig ); - setSiteSettings({ + const nextSiteSettings: SiteSettings = { siteName: siteSettingsData.siteName || "AI Chatbot", siteLogo: siteSettingsData.siteLogo || "", siteLogoDark: siteSettingsData.siteLogoDark || "", @@ -1828,7 +1870,9 @@ export function AdminSettingsClient() { : ["全部", "惊喜"], membershipConfig: normalizedMembershipConfig, membershipUserOverrides: normalizedMembershipOverrides, - }); + }; + setSiteSettings(nextSiteSettings); + lastLoadedSiteSettingsRef.current = nextSiteSettings; } }, [siteSettingsData]); @@ -1840,7 +1884,7 @@ export function AdminSettingsClient() { ? (homeAnimationSettingData.setting.value as Record) : {}; - setHomeAnimationConfig({ + const nextHomeAnimationConfig = { enabled: raw.enabled === true, fileName: String(raw.fileName || "").trim(), htmlContent: @@ -1848,7 +1892,9 @@ export function AdminSettingsClient() { ? raw.htmlContent.replace(/^\uFEFF/, "").replace(/\u0000/g, "") : "", updatedAt: String(raw.updatedAt || "").trim() || null, - }); + }; + setHomeAnimationConfig(nextHomeAnimationConfig); + lastLoadedHomeAnimationConfigRef.current = nextHomeAnimationConfig; }, [homeAnimationSettingData]); useEffect(() => { @@ -3482,25 +3528,42 @@ export function AdminSettingsClient() { const parsedPointsSystemConfig = normalizePointsSystemConfig( siteSettings.pointsSystemConfig ); + const normalizedHomeAnimationConfig: HomeAnimationConfig = { + enabled: homeAnimationConfig.enabled, + fileName: homeAnimationConfig.fileName.trim(), + htmlContent: normalizedHomeAnimationHtml, + updatedAt: homeAnimationConfig.updatedAt, + }; const siteSettingsPayload: Partial = { ...siteSettings, - }; - delete siteSettingsPayload.agentWorkflowUiConfig; - - const payload = { - ...siteSettingsPayload, - homeAnimationConfig: { - enabled: homeAnimationConfig.enabled, - fileName: homeAnimationConfig.fileName.trim(), - htmlContent: normalizedHomeAnimationHtml, - updatedAt: homeAnimationConfig.updatedAt, - }, membershipConfig: parsedMembershipConfig, membershipUserOverrides: parsedMembershipOverrides, pointsSystemConfig: parsedPointsSystemConfig, pointsPackages: parsedPointsPackages, }; + delete siteSettingsPayload.agentWorkflowUiConfig; + + const changedSiteSettingsPayload = pickChangedTopLevelFields( + siteSettingsPayload as Record, + lastLoadedSiteSettingsRef.current as Partial> | null + ) as Partial; + const shouldSaveHomeAnimation = !areComparableValuesEqual( + normalizedHomeAnimationConfig, + lastLoadedHomeAnimationConfigRef.current + ); + + const payload = { + ...changedSiteSettingsPayload, + ...(shouldSaveHomeAnimation + ? { homeAnimationConfig: normalizedHomeAnimationConfig } + : {}), + }; + + if (Object.keys(payload).length === 0) { + toast.info("没有需要保存的改动"); + return; + } const response = await fetch("/api/site-settings", { method: "PUT", @@ -3517,14 +3580,23 @@ export function AdminSettingsClient() { ...prev, ...nextSiteSettings, })); - setHomeAnimationConfig((prev) => ({ - ...prev, - htmlContent: normalizedHomeAnimationHtml, - updatedAt: new Date().toISOString(), - })); + lastLoadedSiteSettingsRef.current = { + ...(lastLoadedSiteSettingsRef.current || siteSettings), + ...siteSettingsPayload, + }; + if (shouldSaveHomeAnimation) { + const committedHomeAnimationConfig: HomeAnimationConfig = { + ...normalizedHomeAnimationConfig, + updatedAt: new Date().toISOString(), + }; + setHomeAnimationConfig(committedHomeAnimationConfig); + lastLoadedHomeAnimationConfigRef.current = committedHomeAnimationConfig; + } toast.success("网站设置保存成功"); mutateSiteSettings(); - mutateHomeAnimationSetting(); + if (shouldSaveHomeAnimation) { + mutateHomeAnimationSetting(); + } } catch (error) { toast.error(error instanceof Error ? error.message : "保存网站设置失败"); } diff --git a/app/(admin)/settings/video-canvas/video-canvas-settings-client.tsx b/app/(admin)/settings/video-canvas/video-canvas-settings-client.tsx index 56bc3e8f7..e90fc5b98 100644 --- a/app/(admin)/settings/video-canvas/video-canvas-settings-client.tsx +++ b/app/(admin)/settings/video-canvas/video-canvas-settings-client.tsx @@ -11,7 +11,6 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, @@ -28,6 +27,7 @@ import { } from "@/lib/ai/model-classification"; import { fetcher } from "@/lib/utils"; import { + getVideoCanvasFunctionBillingCopy, type VideoCanvasFunctionConfig, type VideoCanvasFunctionDefinition, VIDEO_CANVAS_FUNCTION_CONFIG_KEY, @@ -260,7 +260,7 @@ export function VideoCanvasSettingsClient() {

视频画布功能设置

- 这里集中配置复刻画布里的工具能力、预设提示词、默认模型和扣点。前端优先按复刻目标触发,后端缺口可以先留空或后续替换。 + 这里集中配置复刻画布里的工具能力、预设提示词和默认模型。积分与扣费统一跟随全站模型/功能配置,不再在视频画布里单独维护一套价格。

@@ -327,6 +327,9 @@ export function VideoCanvasSettingsClient() {

视频画布运行时会通过 /api/video-canvas/config 读取这里保存的提示词。比如“多机位九宫格”的分镜规则、黑色分割线要求和镜头说明,都由管理员在这里维护,前端不再写死覆盖。

+

+ 计费说明只展示来源,不在这里改价;需要调整积分请回到全站积分设置、模型设置或对应功能设置统一修改。 +

@@ -355,7 +358,7 @@ export function VideoCanvasSettingsClient() { />
-
+
-
- - - updateFunction(item.key, { creditCost: Number(event.target.value || 0) }) - } - type="number" - value={item.creditCost ?? 0} - /> +
+
+ 积分来源:{getVideoCanvasFunctionBillingCopy(item).shortLabel} +
+
+ {getVideoCanvasFunctionBillingCopy(item).description} +
diff --git a/app/api/canvas/3d/tasks/[taskId]/route.ts b/app/api/canvas/3d/tasks/[taskId]/route.ts index 1f454b48c..07e65d98b 100644 --- a/app/api/canvas/3d/tasks/[taskId]/route.ts +++ b/app/api/canvas/3d/tasks/[taskId]/route.ts @@ -6,7 +6,10 @@ import { pickTaskPreviewImageUrl, tripoRequest, } from "@/lib/tripo/client"; -import { getTripoProviderConfig } from "@/lib/tripo/server"; +import { + getTripoProviderConfig, + resolveTripoProviderConfigForTaskId, +} from "@/lib/tripo/server"; import type { TripoTaskData } from "@/lib/tripo/types"; import { uploadFile } from "@/lib/storage/service"; @@ -227,7 +230,10 @@ export async function GET(request: Request, { params }: RouteParams) { } const { searchParams } = new URL(request.url); - const config = await getTripoProviderConfig(searchParams.get("providerId")); + const preferredProviderId = searchParams.get("providerId"); + const config = preferredProviderId + ? await getTripoProviderConfig(preferredProviderId) + : await resolveTripoProviderConfigForTaskId(normalizedTaskId); const result = await tripoRequest(config, { method: "GET", path: `/task/${encodeURIComponent(normalizedTaskId)}`, diff --git a/app/api/canvas/3d/tasks/route.ts b/app/api/canvas/3d/tasks/route.ts index 00e88a0a4..e7eec087e 100644 --- a/app/api/canvas/3d/tasks/route.ts +++ b/app/api/canvas/3d/tasks/route.ts @@ -5,7 +5,10 @@ import { resolveThreeDModelRealId } from "@/lib/model3d/model-alias-config"; import { getThreeDModelAliasConfigForRuntime } from "@/lib/model3d/model-alias-config-server"; import { prepareFeaturePointsCharge } from "@/lib/points/feature-charge"; import { get3DTaskCreditCost } from "@/lib/points/config"; -import { getTripoProviderConfig } from "@/lib/tripo/server"; +import { + getTripoProviderConfig, + resolveTripoProviderConfigForTaskId, +} from "@/lib/tripo/server"; import { TripoApiError, tripoRequest } from "@/lib/tripo/client"; export const runtime = "nodejs"; @@ -366,9 +369,27 @@ export async function POST(request: Request) { return NextResponse.json(pointsCharge.payload, { status: pointsCharge.status }); } - const config = await getTripoProviderConfig( - readOptionalTrimmedText(body.providerId) - ); + const preferredProviderId = readOptionalTrimmedText(body.providerId); + const dependencyTaskId = + taskType === "refine_model" + ? String(body.draftModelTaskId || "").trim() + : taskType === "convert_model" || + taskType === "stylize_model" || + taskType === "texture_model" || + taskType === "animate_prerigcheck" || + taskType === "animate_rig" || + taskType === "animate_retarget" || + taskType === "mesh_segmentation" || + taskType === "mesh_completion" || + taskType === "highpoly_to_lowpoly" + ? String(body.originalModelTaskId || "").trim() + : ""; + const config = dependencyTaskId + ? await resolveTripoProviderConfigForTaskId( + dependencyTaskId, + preferredProviderId, + ) + : await getTripoProviderConfig(preferredProviderId); const result = await tripoRequest>(config, { method: "POST", path: "/task", diff --git a/app/canvas/[id]/model3d/page.tsx b/app/canvas/[id]/model3d/page.tsx index 18a19b652..1cc13cc23 100644 --- a/app/canvas/[id]/model3d/page.tsx +++ b/app/canvas/[id]/model3d/page.tsx @@ -1554,10 +1554,19 @@ export default function CanvasModel3DPage() { exportFormat?: string | null; assetPayload?: Model3DTaskAssetPayload | null; }) => { + const normalizedPayload = + params.provider === "tripo-3d" && + !String(params.payload.providerId || "").trim() && + params.assetPayload?.providerId + ? { + ...params.payload, + providerId: params.assetPayload.providerId, + } + : params.payload; const createResult = params.provider === "worldlabs-3d" - ? await worldLabsGenerationService.createTask(params.payload) - : await model3DGenerationService.createTask(params.payload); + ? await worldLabsGenerationService.createTask(normalizedPayload) + : await model3DGenerationService.createTask(normalizedPayload); if (!createResult.success || !createResult.data?.taskId) { throw new Error(createResult.error || "创建任务失败"); } @@ -1622,6 +1631,8 @@ export default function CanvasModel3DPage() { promptText: string; sourceTaskId: string; successMessage: string; + assetPayload?: Model3DTaskAssetPayload | null; + modelLabel?: string; }) => { setIsSubmitting(true); try { @@ -1630,8 +1641,9 @@ export default function CanvasModel3DPage() { payload: params.payload, mode: params.mode, promptText: params.promptText, - modelLabel: modelVersion, + modelLabel: params.modelLabel || modelVersion, sourceTaskId: params.sourceTaskId, + assetPayload: params.assetPayload ?? null, }); toast.success(params.successMessage); } catch (error) { @@ -2292,6 +2304,7 @@ export default function CanvasModel3DPage() { modelLabel: readyTask.modelVersion, sourceTaskId: baseTaskId, exportFormat, + assetPayload: readyTask.assetPayload ?? null, }); setAutoDownloadTaskId(task.taskId); toast.success(`已提交${exportFormat}转换任务,完成后自动下载`); @@ -2319,6 +2332,7 @@ export default function CanvasModel3DPage() { promptText: `精修任务(来源 ${readyTask.taskId.slice(0, 8)})`, modelLabel: readyTask.modelVersion, sourceTaskId: readyTask.taskId, + assetPayload: readyTask.assetPayload ?? null, }); toast.success("已提交模型精修任务"); } catch (error) { @@ -2348,6 +2362,8 @@ export default function CanvasModel3DPage() { promptText: `纹理重绘(来源 ${readyTask.taskId.slice(0, 8)})`, sourceTaskId: readyTask.taskId, successMessage: "已提交纹理重绘任务", + assetPayload: readyTask.assetPayload ?? null, + modelLabel: readyTask.modelVersion, }); }; @@ -2368,6 +2384,8 @@ export default function CanvasModel3DPage() { promptText: `风格化 ${stylizeStyle}(来源 ${readyTask.taskId.slice(0, 8)})`, sourceTaskId: readyTask.taskId, successMessage: "已提交风格化任务", + assetPayload: readyTask.assetPayload ?? null, + modelLabel: readyTask.modelVersion, }); }; @@ -2384,6 +2402,8 @@ export default function CanvasModel3DPage() { promptText: `可绑定检测(来源 ${readyTask.taskId.slice(0, 8)})`, sourceTaskId: readyTask.taskId, successMessage: "已提交可绑定检测任务", + assetPayload: readyTask.assetPayload ?? null, + modelLabel: readyTask.modelVersion, }); }; @@ -2403,6 +2423,8 @@ export default function CanvasModel3DPage() { promptText: `自动绑定 ${rigType}(来源 ${readyTask.taskId.slice(0, 8)})`, sourceTaskId: readyTask.taskId, successMessage: "已提交自动绑定任务", + assetPayload: readyTask.assetPayload ?? null, + modelLabel: readyTask.modelVersion, }); }; @@ -2424,6 +2446,8 @@ export default function CanvasModel3DPage() { promptText: `动作 ${retargetAnimation}(来源 ${readyTask.taskId.slice(0, 8)})`, sourceTaskId: readyTask.taskId, successMessage: "已提交动作重定向任务", + assetPayload: readyTask.assetPayload ?? null, + modelLabel: readyTask.modelVersion, }); }; @@ -2440,6 +2464,8 @@ export default function CanvasModel3DPage() { promptText: `网格分割(来源 ${readyTask.taskId.slice(0, 8)})`, sourceTaskId: readyTask.taskId, successMessage: "已提交网格分割任务", + assetPayload: readyTask.assetPayload ?? null, + modelLabel: readyTask.modelVersion, }); }; @@ -2462,6 +2488,8 @@ export default function CanvasModel3DPage() { promptText: `网格补全(来源 ${readyTask.taskId.slice(0, 8)})`, sourceTaskId: readyTask.taskId, successMessage: "已提交网格补全任务", + assetPayload: readyTask.assetPayload ?? null, + modelLabel: readyTask.modelVersion, }); }; @@ -2483,6 +2511,8 @@ export default function CanvasModel3DPage() { promptText: `低模优化(来源 ${readyTask.taskId.slice(0, 8)})`, sourceTaskId: readyTask.taskId, successMessage: "已提交低模优化任务", + assetPayload: readyTask.assetPayload ?? null, + modelLabel: readyTask.modelVersion, }); }; diff --git a/components/canvas/LiblibVideoCanvasReplica.tsx b/components/canvas/LiblibVideoCanvasReplica.tsx index fa76c20ad..f7821f6b2 100644 --- a/components/canvas/LiblibVideoCanvasReplica.tsx +++ b/components/canvas/LiblibVideoCanvasReplica.tsx @@ -46,10 +46,18 @@ import { import { toast } from "sonner"; import useSWR from "swr"; import capturedCanvasModules from "@/artifacts/liblib-frontend-capture-ed9a2c12570a49b9a1186017b2c38b52/canvas-modules.json"; +import { getVideoCreditCostFromConfig } from "@/lib/ai/video-model-config"; import { isImageGenerationModelDescriptor, isVideoModelDescriptor, } from "@/lib/ai/model-classification"; +import { + defaultPointsSystemConfig, + getImageToolCreditCost, + normalizePointsSystemConfig, + type ImageToolCreditFeature, + type PointsSystemConfig, +} from "@/lib/points/config"; import { cn, fetcher } from "@/lib/utils"; import { canvasService, @@ -58,6 +66,7 @@ import { workflowAnalysisService, } from "@/services/canvas"; import { + getVideoCanvasFunctionBillingCopy, type VideoCanvasFunctionConfig, type VideoCanvasFunctionDefinition, defaultVideoCanvasFunctionConfig, @@ -251,6 +260,13 @@ type CanvasModelOption = { isDefault?: boolean; isSubModel?: boolean; capabilities?: unknown; + imageGenConfig?: { + creditCostPerImage?: number | null; + creditCostPerVideo?: number | null; + videoDurationOptions?: number[] | null; + creditCostPerVideoByDuration?: Record | null; + creditCostPerVideoByResolution?: Record | null; + } | null; }; type CanvasModelKind = "image" | "video" | "multimodal" | "any"; @@ -312,6 +328,7 @@ function getSiteShowcaseName(siteName?: string | null) { } type MembershipSummaryResponse = { + isHighVip?: boolean; currentPlanId?: string; currentPlan?: { id?: string; @@ -332,6 +349,7 @@ type MembershipSummaryResponse = { remainingThisMonth?: number | null; totalImages?: number | null; }; + pointsSystemConfig?: PointsSystemConfig | null; }; type MembershipPlan = { @@ -1729,6 +1747,130 @@ function getCanvasModelDisplayName( return match?.displayName || normalized; } +type VideoCanvasBillingDisplay = { + label: string; + message: string; +}; + +function getCanvasModelOptionByRef( + modelRef: string | undefined, + models: CanvasModelOption[], +) { + const normalized = normalizeModelRef(modelRef); + if (!normalized) return null; + return ( + models.find( + (model) => + model.id === normalized || + model.modelId === normalized || + getCanvasModelRef(model) === normalized, + ) || null + ); +} + +function getVideoCanvasFallbackBillingDisplay( + definition?: Pick | null, +): VideoCanvasBillingDisplay { + const copy = definition + ? getVideoCanvasFunctionBillingCopy(definition) + : { + shortLabel: "按站点", + description: "沿用站点统一模型与功能积分规则,不在视频画布单独改价。", + }; + return { + label: copy.shortLabel, + message: copy.description, + }; +} + +function resolveVideoCanvasBillingDisplay(params: { + membershipSummary?: MembershipSummaryResponse; + modelOptions?: CanvasModelOption[]; + modelRef?: string; + modelKind?: CanvasModelKind; + imageCount?: number; + duration?: number | null; + resolution?: string | null; + billingFeature?: ImageToolCreditFeature | null; + functionDefinition?: Pick | null; +}): VideoCanvasBillingDisplay { + const fallback = getVideoCanvasFallbackBillingDisplay(params.functionDefinition); + const pointsConfig = normalizePointsSystemConfig( + params.membershipSummary?.pointsSystemConfig || defaultPointsSystemConfig, + ); + + if (params.membershipSummary?.isHighVip) { + return { + label: "VIP", + message: "当前高阶会员按站点规则免扣积分。", + }; + } + + if (pointsConfig.enabled !== true) { + return { + label: "免费", + message: "当前站点未启用积分扣费。", + }; + } + + if (params.billingFeature) { + const featureCost = getImageToolCreditCost(pointsConfig, params.billingFeature); + if (featureCost > 0) { + return { + label: String(featureCost), + message: `当前功能按全站积分设置消耗 ${featureCost} 积分。`, + }; + } + return fallback; + } + + const matchedModel = getCanvasModelOptionByRef( + params.modelRef, + params.modelOptions || [], + ); + if (!matchedModel?.imageGenConfig) { + return fallback; + } + + if (params.modelKind === "video") { + const videoCost = getVideoCreditCostFromConfig( + matchedModel.imageGenConfig, + params.duration, + params.resolution, + ); + if (videoCost > 0) { + return { + label: String(videoCost), + message: `当前视频模型按站点配置预计消耗 ${videoCost} 积分。`, + }; + } + return fallback; + } + + if (params.modelKind === "image") { + const unitCost = Math.max( + 0, + Math.round(Number(matchedModel.imageGenConfig.creditCostPerImage ?? 0)), + ); + if (unitCost > 0) { + const safeImageCount = Math.max( + 1, + Math.round(Number(params.imageCount || 1)), + ); + const totalCost = unitCost * safeImageCount; + return { + label: String(unitCost), + message: + safeImageCount > 1 + ? `当前图片模型按站点配置 ${unitCost} 积分/张,本次共 ${totalCost} 积分。` + : `当前图片模型按站点配置消耗 ${unitCost} 积分/张。`, + }; + } + } + + return fallback; +} + function composeFunctionPrompt( definition: VideoCanvasFunctionDefinition, userPrompt?: string, @@ -2552,6 +2694,10 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib "/api/models", fetcher ); + const { data: membershipSummary } = useSWR( + "/api/user/membership-summary", + fetcher + ); const videoCanvasFunctionConfig = useMemo( () => normalizeVideoCanvasFunctionConfig(videoCanvasFunctionSettings?.config), [videoCanvasFunctionSettings?.config] @@ -5398,6 +5544,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib {selectedPresetAnchor && selectedPresetControlNode ? ( { @@ -5509,6 +5661,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib void; onCreateNodes: () => void; onUpscale: (scaleFactor: number) => void; @@ -6287,7 +6440,7 @@ function GridSplitActionToolbar({ 生成高清图片 - ↑{Math.max(0, creditCost)} + {billingLabel}
@@ -6700,6 +6853,7 @@ function VideoAnalysisFloatingPanel({ function PresetWorkflowControls({ anchor, functionConfig, + membershipSummary, mode, modelOptions, nodes, @@ -6713,6 +6867,7 @@ function PresetWorkflowControls({ }: { anchor: PresetControlAnchor; functionConfig: VideoCanvasFunctionConfig; + membershipSummary?: MembershipSummaryResponse; mode: Exclude; modelOptions: CanvasModelOption[]; nodes: CloneNode[]; @@ -6732,7 +6887,9 @@ function PresetWorkflowControls({
@@ -6829,13 +6993,17 @@ function PresetWorkflowControls({ function TextTargetModuleComposer({ anchor, + billingDefinition, defaultModel = "auto", + membershipSummary, modelOptions = [], node, onSubmit, }: { anchor: PresetControlAnchor; + billingDefinition?: VideoCanvasFunctionDefinition; defaultModel?: string; + membershipSummary?: MembershipSummaryResponse; modelOptions?: CanvasModelOption[]; node: CloneNode; onSubmit: (prompt: string, model: string) => void; @@ -6859,6 +7027,18 @@ function TextTargetModuleComposer({ setSelectedModel(initialModel); }, [initialModel, node.id]); + const billingDisplay = useMemo( + () => + resolveVideoCanvasBillingDisplay({ + functionDefinition: billingDefinition, + membershipSummary, + modelKind: "multimodal", + modelOptions, + modelRef: selectedModel, + }), + [billingDefinition, membershipSummary, modelOptions, selectedModel], + ); + return (
toast.info("翻译接口待接入。")} /> - toast.info("积分估算接入本地积分系统。")} /> + toast.info(billingDisplay.message)} + />
void; @@ -7909,6 +8141,21 @@ function PresetComposerPanel({ setSettings((current) => ({ ...current, model: normalizeModelRef(defaultModel) || "auto" })); }, [defaultModel]); + const billingDisplay = useMemo( + () => + resolveVideoCanvasBillingDisplay({ + duration: modelKind === "video" ? 5 : null, + functionDefinition: billingDefinition, + imageCount: 1, + membershipSummary, + modelKind, + modelOptions, + modelRef: settings.model, + resolution: modelKind === "video" ? "720p" : null, + }), + [billingDefinition, membershipSummary, modelKind, modelOptions, settings.model], + ); + return (
toast.info("清晰度选择接口待接入。")} /> toast.info("镜头配置接口待接入。")} /> toast.info("生成数量接口待接入。")} /> - toast.info("积分估算接口待接入。")} /> + toast.info(billingDisplay.message)} + />