Your Name d72ca0b344
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: polish style pro model visibility and billing
2026-05-20 14:26:24 +08:00

289 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import "server-only";
import { prepareFeaturePointsCharge } from "@/lib/points/feature-charge";
import {
generateCanvasImage,
type CanvasGenerateImageResult,
} from "@/lib/canvas/generate-image";
import type {
StyleProGenerationMode,
StyleProCustomInputMap,
StyleProSelectionMap,
} from "./config";
export type StyleProTaskSelectedOption = {
categoryId: string;
categoryTitle: string;
jsonKey: string;
optionId: string;
optionLabel: string;
prompt: string;
};
export type StyleProTaskCustomInput = {
scope: "global" | "category";
fieldId: string;
fieldLabel: string;
fieldType: string;
value: string;
prompt: string;
categoryId?: string;
categoryTitle?: string;
};
export type StyleProTaskModelRef = {
id: string;
displayName: string;
};
export type StyleProTaskBilling = {
unitCost: number;
reason: string;
modelId: string;
messageId?: string | null;
note: string;
};
export type StyleProTaskRequest = {
taskKind: "style-pro";
canvasId?: string | null;
sourceImageUrl: string;
generationMode: StyleProGenerationMode;
normalizedSelections: StyleProSelectionMap;
customInputs?: StyleProCustomInputMap;
selectedOptions: StyleProTaskSelectedOption[];
selectedCustomInputs?: StyleProTaskCustomInput[];
selectedModels: StyleProTaskModelRef[];
optionLabels: string[];
prompts: {
defaultPrompt: string;
realisticFirstPrompt?: string;
realisticSecondPrompt?: string;
negativePrompt?: string;
aspectRatio?: string;
imageSize?: string;
};
billing: StyleProTaskBilling;
requestedStyleProModelId?: string | null;
};
export type StyleProTaskResult = {
imageUrl: string;
thumbnailUrl?: string;
model?: string;
modelRef?: string;
generationMode: StyleProGenerationMode;
chargedPoints: number;
selections: StyleProSelectionMap;
customInputs?: StyleProCustomInputMap;
optionLabels: string[];
};
function composeStyleProPrompt({
defaultPrompt,
selectedOptions,
selectedCustomInputs = [],
mode = "style-pro-final",
}: {
defaultPrompt: string;
selectedOptions: StyleProTaskSelectedOption[];
selectedCustomInputs?: StyleProTaskCustomInput[];
mode?: string;
}) {
const groupedPrompt = selectedOptions.reduce<Record<string, string[]>>(
(acc, item) => {
const key = item.jsonKey || item.categoryId;
acc[key] = acc[key] || [];
acc[key].push(item.prompt);
return acc;
},
{}
);
const customPrompt = selectedCustomInputs.reduce<Record<string, string[]>>(
(acc, item) => {
const key =
item.scope === "category" && item.categoryTitle
? item.categoryTitle
: "全局自定义";
acc[key] = acc[key] || [];
acc[key].push(item.prompt || `${item.fieldLabel}:${item.value}`);
return acc;
},
{}
);
return [
defaultPrompt,
"请严格依据以下风格化 PRO 选择执行最终图片生成。用户不可见配置 JSON:",
JSON.stringify(
{
mode,
selections: selectedOptions,
promptByKey: groupedPrompt,
customInputs: selectedCustomInputs,
customPromptByScope: customPrompt,
},
null,
2
),
]
.filter(Boolean)
.join("\n\n");
}
async function consumeStyleProTaskPoints(
userId: string,
billing: StyleProTaskBilling
) {
if (!Number.isFinite(billing.unitCost) || billing.unitCost <= 0) {
return 0;
}
try {
const pointsCharge = await prepareFeaturePointsCharge({
userId,
unitCost: billing.unitCost,
reason: billing.reason,
modelId: billing.modelId,
messageId: billing.messageId || null,
note: billing.note,
});
if (!pointsCharge.ok || !pointsCharge.chargeable) {
return 0;
}
const consumeResult = await pointsCharge.consume();
if (consumeResult?.ok && !consumeResult.skipped) {
return billing.unitCost;
}
} catch (error) {
console.error("[Style Pro Task] Failed to consume points:", error);
}
return 0;
}
function getPrimaryModelId(request: StyleProTaskRequest) {
return String(request.selectedModels[0]?.id || "").trim();
}
export function getStyleProTaskPrimaryModelKey(request: StyleProTaskRequest) {
return getPrimaryModelId(request) || "__default__";
}
function normalizeCanvasImageResultImage(
result: CanvasGenerateImageResult,
fallbackError: string
) {
const image = result.images[0];
const imageUrl = String(image?.originalUrl || image?.thumbnailUrl || "").trim();
if (!imageUrl) {
throw new Error(fallbackError);
}
return {
imageUrl,
thumbnailUrl: String(image?.thumbnailUrl || image?.originalUrl || "").trim() || imageUrl,
model: result.model,
modelRef: result.modelRef,
};
}
export async function executeStyleProTask(params: {
request: StyleProTaskRequest;
userId: string;
}) {
const { request, userId } = params;
const aspectRatio = request.prompts.aspectRatio || "auto";
const imageSize = request.prompts.imageSize || "2K";
const negativePrompt = request.prompts.negativePrompt || undefined;
const result =
request.generationMode === "realistic"
? await (async () => {
const firstModelId = getPrimaryModelId(request);
const secondModelId = String(request.selectedModels[1]?.id || "").trim();
if (!firstModelId || !secondModelId) {
throw new Error("请先在后台配置实景化两段隐藏模型");
}
const firstResult = await generateCanvasImage({
prompt: request.prompts.realisticFirstPrompt || "",
negativePrompt,
model: firstModelId,
aspectRatio,
imageSize,
imageCount: 1,
referenceImage: request.sourceImageUrl,
ownerKey: userId,
debugLabel: "style-pro-realistic-stage1",
debugMeta: {
canvasId: request.canvasId || null,
selectedOptionCount: request.selectedOptions.length,
},
});
const firstPassImage = normalizeCanvasImageResultImage(
firstResult,
"实景化第一阶段生成失败"
);
return generateCanvasImage({
prompt: composeStyleProPrompt({
defaultPrompt: request.prompts.realisticSecondPrompt || "",
selectedOptions: request.selectedOptions,
selectedCustomInputs: request.selectedCustomInputs || [],
mode: "style-pro-realistic-final",
}),
negativePrompt,
model: secondModelId,
aspectRatio,
imageSize,
imageCount: 1,
referenceImage: firstPassImage.imageUrl,
ownerKey: userId,
debugLabel: "style-pro-realistic-stage2",
debugMeta: {
canvasId: request.canvasId || null,
selectedOptionCount: request.selectedOptions.length,
firstStageModelRef: firstModelId,
},
});
})()
: await generateCanvasImage({
prompt: composeStyleProPrompt({
defaultPrompt: request.prompts.defaultPrompt,
selectedOptions: request.selectedOptions,
selectedCustomInputs: request.selectedCustomInputs || [],
}),
negativePrompt,
model: getPrimaryModelId(request),
aspectRatio,
imageSize,
imageCount: 1,
referenceImage: request.sourceImageUrl,
ownerKey: userId,
debugLabel: "style-pro",
debugMeta: {
canvasId: request.canvasId || null,
requestedStyleProModelId: request.requestedStyleProModelId || null,
selectedOptionCount: request.selectedOptions.length,
},
});
const finalImage = normalizeCanvasImageResultImage(result, "生成结果为空");
const chargedPoints = await consumeStyleProTaskPoints(userId, request.billing);
return {
imageUrl: finalImage.imageUrl,
thumbnailUrl: finalImage.thumbnailUrl,
model: finalImage.model,
modelRef: finalImage.modelRef,
generationMode: request.generationMode,
chargedPoints,
selections: request.normalizedSelections,
customInputs: request.customInputs,
optionLabels: request.optionLabels,
} satisfies StyleProTaskResult;
}