feat: update enhance pro configuration
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-05-07 19:54:02 +08:00
parent b8e8b45b6e
commit 66fd760b9a
8 changed files with 525 additions and 95 deletions

View File

@ -38,6 +38,7 @@ import {
normalizeDetailAgentLocalMoonshotConfig,
} from "@/lib/ai/detail-agent-model-config";
import {
ENHANCE_PRO_KREA_MODELS,
ENHANCE_PRO_KREA_DEFAULT_BASE_URL,
ENHANCE_PRO_KREA_DEFAULT_MODEL,
ENHANCE_PRO_KREA_DEFAULT_OUTPUT_FORMAT,
@ -45,6 +46,7 @@ import {
defaultEnhanceProKreaConfig,
defaultEnhanceProPresets,
normalizeEnhanceProKreaConfig,
type EnhanceProKreaModel,
type EnhanceProPreset,
} from "@/lib/enhance-pro/config";
import {
@ -4521,6 +4523,39 @@ export function AdminSettingsClient() {
}));
};
const handleToggleEnhanceProModelVisible = (
model: EnhanceProKreaModel,
visible: boolean
) => {
setEnhanceProConfig((prev) => {
const hiddenModels = new Set(prev.hiddenModels || []);
if (visible) {
hiddenModels.delete(model);
} else {
hiddenModels.add(model);
}
const nextHiddenModels = ENHANCE_PRO_KREA_MODELS.filter((item) =>
hiddenModels.has(item)
);
const visibleModels = ENHANCE_PRO_KREA_MODELS.filter(
(item) => !nextHiddenModels.includes(item)
);
if (visibleModels.length === 0) {
toast.error("至少需要保留一个前台可见模型");
return prev;
}
return {
...prev,
hiddenModels: nextHiddenModels,
defaultModel: visibleModels.includes(prev.defaultModel)
? prev.defaultModel
: visibleModels[0] || ENHANCE_PRO_KREA_DEFAULT_MODEL,
};
});
};
const handleEnhanceProPresetChange = <K extends keyof EnhanceProPreset>(
index: number,
key: K,
@ -7176,10 +7211,15 @@ export function AdminSettingsClient() {
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Redefine">Redefine</SelectItem>
<SelectItem value="Recovery">Recovery</SelectItem>
<SelectItem value="Recovery V2">Recovery V2</SelectItem>
<SelectItem value="Reimagine">Reimagine</SelectItem>
{ENHANCE_PRO_KREA_MODELS.map((model) => {
const isHidden = enhanceProConfig.hiddenModels.includes(model);
return (
<SelectItem key={model} value={model}>
{model}
{isHidden ? "(前台隐藏)" : ""}
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
@ -10415,14 +10455,7 @@ export function AdminSettingsClient() {
<div className="space-y-2">
<Label>模型倍率</Label>
<div className="grid grid-cols-1 gap-3 md:grid-cols-4">
{(
[
"Redefine",
"Recovery",
"Recovery V2",
"Reimagine",
] as const
).map((modelKey) => (
{ENHANCE_PRO_KREA_MODELS.map((modelKey) => (
<div key={modelKey} className="rounded-lg border p-3">
<div className="space-y-1">
<Label className="text-xs text-muted-foreground">
@ -18615,11 +18648,7 @@ export function AdminSettingsClient() {
onValueChange={(value) =>
setEnhanceProConfig((prev) => ({
...prev,
defaultModel: value as
| "Redefine"
| "Recovery"
| "Recovery V2"
| "Reimagine",
defaultModel: value as EnhanceProKreaModel,
}))
}
>
@ -18627,10 +18656,20 @@ export function AdminSettingsClient() {
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Redefine">Redefine</SelectItem>
<SelectItem value="Recovery">Recovery</SelectItem>
<SelectItem value="Recovery V2">Recovery V2</SelectItem>
<SelectItem value="Reimagine">Reimagine</SelectItem>
{ENHANCE_PRO_KREA_MODELS.map((model) => {
const isHidden =
enhanceProConfig.hiddenModels.includes(model);
return (
<SelectItem
key={model}
value={model}
disabled={isHidden}
>
{model}
{isHidden ? "(已隐藏)" : ""}
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
@ -18659,6 +18698,44 @@ export function AdminSettingsClient() {
</div>
</div>
<div className="space-y-3 rounded-lg border p-4">
<div className="space-y-1">
<Label className="text-sm">前台可见模型</Label>
<div className="text-xs text-muted-foreground">
关闭后用户端不会展示该模型,旧客户端继续请求时服务端也会直接拒绝,适合临时屏蔽异常模型。
</div>
</div>
<div className="grid gap-3 md:grid-cols-4">
{ENHANCE_PRO_KREA_MODELS.map((model) => {
const isVisible =
!enhanceProConfig.hiddenModels.includes(model);
const visibleCount = ENHANCE_PRO_KREA_MODELS.filter(
(item) => !enhanceProConfig.hiddenModels.includes(item)
).length;
return (
<div
key={model}
className="flex items-center justify-between rounded-lg border px-3 py-2"
>
<div className="space-y-0.5">
<div className="text-sm font-medium">{model}</div>
<div className="text-xs text-muted-foreground">
{isVisible ? "用户可选" : "已隐藏"}
</div>
</div>
<Switch
checked={isVisible}
disabled={isVisible && visibleCount <= 1}
onCheckedChange={(checked) =>
handleToggleEnhanceProModelVisible(model, checked)
}
/>
</div>
);
})}
</div>
</div>
<div className="grid gap-4 md:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="enhanceProMaxBatchSize">最大候选图数量</Label>

View File

@ -10,6 +10,7 @@ import {
getMediaAssetsByUserAndUrls,
getSystemSetting,
getUserById,
refundUserPoints,
upsertMediaAsset,
} from "@/lib/db/queries";
import {
@ -19,6 +20,8 @@ import {
} from "@/lib/db/schema";
import {
ENHANCE_PRO_KREA_SETTING_KEY,
isEnhanceProKreaModel,
isEnhanceProKreaModelHidden,
normalizeEnhanceProKreaConfig,
type EnhanceProKreaConfig,
type EnhanceProKreaModel,
@ -555,13 +558,7 @@ function parseEnhanceProStoredRequest(task: EnhanceProTaskRecord): EnhanceProSto
width,
height,
batchSize: clampInteger(request.batchSize, 1, 8, 1),
model:
request.model === "Recovery" ||
request.model === "Recovery V2" ||
request.model === "Reimagine" ||
request.model === "Redefine"
? request.model
: "Recovery V2",
model: isEnhanceProKreaModel(request.model) ? request.model : "Recovery V2",
upscalingActivated: normalizeBoolean(request.upscalingActivated, false),
imageScalingFactor: clampNumber(request.imageScalingFactor, 1, 8, 1),
chatId: request.chatId ? String(request.chatId) : task.chatId || null,
@ -578,6 +575,15 @@ async function getEnhanceProTaskForUser(params: { taskId: string; userId: string
return rows[0] ?? null;
}
async function getEnhanceProTaskById(taskId: string) {
const rows = await db
.select()
.from(enhanceProTask)
.where(eq(enhanceProTask.id, taskId))
.limit(1);
return rows[0] ?? null;
}
async function getEnhanceProTaskByClientRequestId(params: {
userId: string;
clientRequestId: string;
@ -595,6 +601,94 @@ async function getEnhanceProTaskByClientRequestId(params: {
return rows[0] ?? null;
}
function getEnhanceProPointsChargeNote(task: EnhanceProTaskRecord) {
return `图像增强 Pro 消耗积分(任务 ${task.id} / ${task.providerTaskId})`;
}
function getEnhanceProPointsRefundNote(task: EnhanceProTaskRecord) {
return `图像增强 Pro 失败退回积分(任务 ${task.id} / ${task.providerTaskId})`;
}
async function refundEnhanceProPointsForFailedTask(
task: EnhanceProTaskRecord,
errorMessage: string,
) {
if (task.pointsCost <= 0) {
return 0;
}
const chargeLedger = await db
.select({ id: pointsLedger.id })
.from(pointsLedger)
.where(
and(
eq(pointsLedger.userId, task.userId),
eq(pointsLedger.reason, "enhance_pro_generation"),
eq(pointsLedger.note, getEnhanceProPointsChargeNote(task)),
),
)
.limit(1);
if (!chargeLedger[0]) {
return 0;
}
const refundNote = getEnhanceProPointsRefundNote(task);
const existingRefundLedger = await db
.select({ id: pointsLedger.id })
.from(pointsLedger)
.where(
and(
eq(pointsLedger.userId, task.userId),
eq(pointsLedger.reason, "enhance_pro_refund"),
eq(pointsLedger.note, refundNote),
),
)
.limit(1);
if (existingRefundLedger[0]) {
await db
.update(enhanceProTask)
.set({
pointsCharged: false,
chargedAt: null,
updatedAt: new Date(),
})
.where(eq(enhanceProTask.id, task.id));
return task.pointsCost;
}
const request = parseEnhanceProStoredRequest(task);
const refundResult = await refundUserPoints({
userId: task.userId,
amount: task.pointsCost,
reason: "enhance_pro_refund",
modelId: `${ENHANCE_PRO_MODEL_ID}:${request.model}`,
chatId: request.chatId,
messageId: request.messageId,
note: refundNote,
});
if (!refundResult.ok) {
console.error("[Points] Failed to refund Enhance Pro points:", {
taskId: task.id,
errorMessage,
});
return 0;
}
await db
.update(enhanceProTask)
.set({
pointsCharged: false,
chargedAt: null,
updatedAt: new Date(),
})
.where(eq(enhanceProTask.id, task.id));
return task.pointsCost;
}
async function markEnhanceProTaskRunning(taskId: string) {
await db
.update(enhanceProTask)
@ -625,6 +719,16 @@ async function claimEnhanceProTaskFinalization(taskId: string) {
}
async function markEnhanceProTaskFailed(taskId: string, errorMessage: string) {
const task = await getEnhanceProTaskById(taskId);
let refundedPoints = 0;
if (task) {
try {
refundedPoints = await refundEnhanceProPointsForFailedTask(task, errorMessage);
} catch (refundError) {
console.error("[Points] Failed to refund failed Enhance Pro task:", refundError);
}
}
await db
.update(enhanceProTask)
.set({
@ -634,6 +738,8 @@ async function markEnhanceProTaskFailed(taskId: string, errorMessage: string) {
updatedAt: new Date(),
})
.where(eq(enhanceProTask.id, taskId));
return { refundedPoints };
}
async function persistEnhanceProTaskSuccess(params: {
@ -660,10 +766,10 @@ async function consumeEnhanceProPointsOnce(
request: EnhanceProStoredRequest,
) {
if (task.pointsCost <= 0) {
return { ok: true, skipped: true };
return { ok: true, skipped: true, chargeApplied: false };
}
const note = `图像增强 Pro 消耗积分(任务 ${task.id} / ${task.providerTaskId})`;
const note = getEnhanceProPointsChargeNote(task);
const existingLedger = await db
.select({ id: pointsLedger.id })
.from(pointsLedger)
@ -677,10 +783,10 @@ async function consumeEnhanceProPointsOnce(
.limit(1);
if (existingLedger[0]) {
return { ok: true, skipped: true };
return { ok: true, skipped: true, chargeApplied: true };
}
return await consumeUserPoints({
const result = await consumeUserPoints({
userId: task.userId,
amount: task.pointsCost,
reason: "enhance_pro_generation",
@ -689,6 +795,10 @@ async function consumeEnhanceProPointsOnce(
messageId: request.messageId,
note,
});
return {
...result,
chargeApplied: result.ok && !result.skipped,
};
}
function getStoredEnhanceProSuccessPayload(task: EnhanceProTaskRecord) {
@ -733,7 +843,7 @@ async function finalizeEnhanceProTask(
if (!pointsCharged && claimedTask.pointsCost > 0) {
try {
const chargeResult = await consumeEnhanceProPointsOnce(claimedTask, request);
pointsCharged = chargeResult.ok || chargeResult.skipped;
pointsCharged = chargeResult.chargeApplied === true;
} catch (pointsError) {
console.error("[Points] Failed to consume Enhance Pro points:", pointsError);
}
@ -758,11 +868,15 @@ async function finalizeEnhanceProTask(
}
}
function buildEnhanceProTaskFailedResponse(task: EnhanceProTaskRecord) {
function buildEnhanceProTaskFailedResponse(
task: EnhanceProTaskRecord,
refundedPoints = 0,
) {
return NextResponse.json(
{
error: "图像增强 Pro 处理失败",
detail: String(task.error || "任务失败"),
refundedPoints,
},
{ status: 500 },
);
@ -805,7 +919,11 @@ export async function GET(request: NextRequest) {
return NextResponse.json(storedPayload);
}
if (task.status === "failed") {
return buildEnhanceProTaskFailedResponse(task);
const failureUpdate = await markEnhanceProTaskFailed(
task.id,
String(task.error || "任务失败"),
);
return buildEnhanceProTaskFailedResponse(task, failureUpdate.refundedPoints);
}
if (task.status === "finalizing") {
return buildEnhanceProTaskPendingResponse(task.id);
@ -850,11 +968,12 @@ export async function GET(request: NextRequest) {
if (providerStatus === "failed" || providerStatus === "cancelled") {
const detail = `Krea 任务${providerStatus === "failed" ? "失败" : "已取消"}`;
await markEnhanceProTaskFailed(task.id, detail);
const failureUpdate = await markEnhanceProTaskFailed(task.id, detail);
return NextResponse.json(
{
error: "图像增强 Pro 处理失败",
detail,
refundedPoints: failureUpdate.refundedPoints,
},
{ status: 500 },
);
@ -901,7 +1020,14 @@ export async function POST(request: NextRequest) {
return NextResponse.json(existingPayload);
}
if (existingTask.status === "failed") {
return buildEnhanceProTaskFailedResponse(existingTask);
const failureUpdate = await markEnhanceProTaskFailed(
existingTask.id,
String(existingTask.error || "任务失败"),
);
return buildEnhanceProTaskFailedResponse(
existingTask,
failureUpdate.refundedPoints,
);
}
return buildEnhanceProTaskPendingResponse(existingTask.id);
}
@ -910,6 +1036,22 @@ export async function POST(request: NextRequest) {
const prompt = String(body.prompt || "").trim();
const chatId = body.chatId || null;
const messageId = body.messageId || null;
const requestedModel = String(body.model || "").trim();
const selectedModel: EnhanceProKreaModel =
isEnhanceProKreaModel(requestedModel)
? requestedModel
: config.defaultModel;
if (isEnhanceProKreaModelHidden(config, selectedModel)) {
return NextResponse.json(
{
error: "当前图像增强 Pro 模型已隐藏,请切换其他模型",
code: "enhance_pro_model_hidden",
model: selectedModel,
},
{ status: 403 },
);
}
const sourceInput = await resolveSourceInput(body);
const requestedWidth = sanitizeDimension(body.width, sourceInput.width);
@ -923,13 +1065,6 @@ export async function POST(request: NextRequest) {
const batchSize = config.allowBatchSize
? clampInteger(body.batchSize, 1, config.maxBatchSize, 1)
: 1;
const selectedModel: EnhanceProKreaModel =
body.model === "Recovery" ||
body.model === "Recovery V2" ||
body.model === "Reimagine" ||
body.model === "Redefine"
? body.model
: config.defaultModel;
const upscalingActivated = body.upscaling_activated === true;
const imageScalingFactor = clampNumber(
body.image_scaling_factor,
@ -1050,7 +1185,14 @@ export async function POST(request: NextRequest) {
return NextResponse.json(existingPayload);
}
if (existingTask.status === "failed") {
return buildEnhanceProTaskFailedResponse(existingTask);
const failureUpdate = await markEnhanceProTaskFailed(
existingTask.id,
String(existingTask.error || "任务失败"),
);
return buildEnhanceProTaskFailedResponse(
existingTask,
failureUpdate.refundedPoints,
);
}
return buildEnhanceProTaskPendingResponse(existingTask.id);
}

View File

@ -25,6 +25,8 @@ function mapLedgerReasonLabel(reason: string) {
if (reason === "model3d_generation") return "3D模型生成";
if (reason === "world3d_generation") return "3D世界生成";
if (reason === "video_generation") return "视频生成";
if (reason === "enhance_pro_generation") return "图像增强 Pro";
if (reason === "enhance_pro_refund") return "图像增强 Pro 失败退回";
if (reason === "deduct") return "管理员扣减";
if (reason === "gift") return "管理员赠送";
if (reason === "recharge") return "积分充值";

View File

@ -42,6 +42,7 @@ import { Switch } from "@/components/ui/switch";
import { Slider } from "@/components/ui/slider";
import { Textarea } from "@/components/ui/textarea";
import {
ENHANCE_PRO_KREA_MODELS,
defaultEnhanceProPresets,
type EnhanceProKreaModel,
type EnhanceProOutputFormat,
@ -108,6 +109,7 @@ type EnhanceProClientConfig = {
providerName: string;
baseUrl: string;
defaultModel: EnhanceProKreaModel;
availableModels: EnhanceProKreaModel[];
defaultOutputFormat: EnhanceProOutputFormat;
allowBatchSize: boolean;
maxBatchSize: number;
@ -150,6 +152,7 @@ const FALLBACK_CONFIG: EnhanceProClientConfig = {
providerName: "Krea Topaz Generative",
baseUrl: "https://api.krea.ai",
defaultModel: "Redefine",
availableModels: [...ENHANCE_PRO_KREA_MODELS],
defaultOutputFormat: "jpg",
allowBatchSize: true,
maxBatchSize: 4,
@ -352,6 +355,26 @@ function fitDimensions(width: number, height: number, maxOutputEdge: number) {
};
}
function getAvailableEnhanceProModels(config: EnhanceProClientConfig) {
return config.availableModels?.length
? config.availableModels
: [...ENHANCE_PRO_KREA_MODELS];
}
function resolveEnhanceProModel(
config: EnhanceProClientConfig,
model?: EnhanceProKreaModel
) {
const availableModels = getAvailableEnhanceProModels(config);
if (model && availableModels.includes(model)) {
return model;
}
if (availableModels.includes(config.defaultModel)) {
return config.defaultModel;
}
return availableModels[0] || "Redefine";
}
function createDefaultFormState(
config: EnhanceProClientConfig,
width: number,
@ -370,7 +393,7 @@ function createDefaultFormState(
face_enhancement_creativity: 0.5,
face_enhancement_strength: 0.5,
crop_to_fill: false,
model: config.defaultModel,
model: resolveEnhanceProModel(config, config.defaultModel),
upscaling_activated: false,
image_scaling_factor: 1,
creativity: 3,
@ -416,7 +439,7 @@ function applyPresetToForm(
1
),
crop_to_fill: preset.cropToFill,
model: preset.model,
model: resolveEnhanceProModel(config, preset.model),
upscaling_activated: preset.upscalingActivated,
image_scaling_factor: clampNumber(
preset.imageScalingFactor,
@ -593,8 +616,16 @@ export function EnhanceProWorkspace({
[configPayload?.pointsConfig]
);
const presets = useMemo(
() => (config.presets.length ? config.presets : FALLBACK_CONFIG.presets),
[config.presets]
() => {
const availableModels = getAvailableEnhanceProModels(config);
return (config.presets.length ? config.presets : FALLBACK_CONFIG.presets)
.filter((preset) => availableModels.includes(preset.model));
},
[config]
);
const availableModels = useMemo(
() => getAvailableEnhanceProModels(config),
[config]
);
const selectedPreset = useMemo(
() => presets.find((item) => item.id === selectedPresetId) || null,
@ -796,7 +827,7 @@ export function EnhanceProWorkspace({
1
),
crop_to_fill: formState.crop_to_fill,
model: formState.model,
model: resolveEnhanceProModel(config, formState.model),
upscaling_activated: formState.upscaling_activated,
image_scaling_factor: clampNumber(
formState.image_scaling_factor,
@ -845,7 +876,7 @@ export function EnhanceProWorkspace({
scopeKey: resolvedHistoryScopeKey,
createdAt: historyCreatedAt + index,
prompt: requestParams.prompt || "",
model: requestParams.model || config.defaultModel,
model: requestParams.model || resolveEnhanceProModel(config, config.defaultModel),
image,
}))
)
@ -898,7 +929,7 @@ export function EnhanceProWorkspace({
sourceImageUrl: sourceImageUrl || undefined,
prompt: formState.prompt.trim() || "图像增强 Pro",
title: "图像增强 Pro 结果",
model: selectedPreset?.model || config.defaultModel,
model: selectedPreset?.model || resolveEnhanceProModel(config, config.defaultModel),
width: selectedGeneratedImage.width,
height: selectedGeneratedImage.height,
});
@ -1472,10 +1503,11 @@ export function EnhanceProWorkspace({
<SelectValue />
</SelectTrigger>
<SelectContent className={SOLID_PANEL_SELECT_CONTENT_CLASS}>
<SelectItem value="Redefine">Redefine</SelectItem>
<SelectItem value="Recovery">Recovery</SelectItem>
<SelectItem value="Recovery V2">Recovery V2</SelectItem>
<SelectItem value="Reimagine">Reimagine</SelectItem>
{availableModels.map((model) => (
<SelectItem key={model} value={model}>
{model}
</SelectItem>
))}
</SelectContent>
</Select>
</div>

View File

@ -20,6 +20,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import {
ENHANCE_PRO_KREA_MODELS,
defaultEnhanceProPresets,
type EnhanceProKreaModel,
type EnhanceProOutputFormat,
@ -73,6 +74,7 @@ type EnhanceProClientConfig = {
providerName: string;
baseUrl: string;
defaultModel: EnhanceProKreaModel;
availableModels: EnhanceProKreaModel[];
defaultOutputFormat: EnhanceProOutputFormat;
allowBatchSize: boolean;
maxBatchSize: number;
@ -109,6 +111,7 @@ const FALLBACK_CONFIG: EnhanceProClientConfig = {
providerName: "Krea Topaz Generative",
baseUrl: "https://api.krea.ai",
defaultModel: "Redefine",
availableModels: [...ENHANCE_PRO_KREA_MODELS],
defaultOutputFormat: "jpg",
allowBatchSize: true,
maxBatchSize: 4,
@ -139,6 +142,26 @@ function fitDimensions(width: number, height: number, maxOutputEdge: number) {
};
}
function getAvailableEnhanceProModels(config: EnhanceProClientConfig) {
return config.availableModels?.length
? config.availableModels
: [...ENHANCE_PRO_KREA_MODELS];
}
function resolveEnhanceProModel(
config: EnhanceProClientConfig,
model?: EnhanceProKreaModel,
) {
const availableModels = getAvailableEnhanceProModels(config);
if (model && availableModels.includes(model)) {
return model;
}
if (availableModels.includes(config.defaultModel)) {
return config.defaultModel;
}
return availableModels[0] || "Redefine";
}
function createDefaultFormState(config: EnhanceProClientConfig, width: number, height: number): FormState {
const size = fitDimensions(width, height, config.maxOutputEdge);
return {
@ -153,7 +176,7 @@ function createDefaultFormState(config: EnhanceProClientConfig, width: number, h
face_enhancement_creativity: 0.5,
face_enhancement_strength: 0.5,
crop_to_fill: false,
model: config.defaultModel,
model: resolveEnhanceProModel(config, config.defaultModel),
upscaling_activated: false,
image_scaling_factor: 1,
creativity: 3,
@ -189,7 +212,7 @@ function applyPresetToForm(
face_enhancement_creativity: clampNumber(preset.faceEnhancementCreativity, 0, 1),
face_enhancement_strength: clampNumber(preset.faceEnhancementStrength, 0, 1),
crop_to_fill: preset.cropToFill,
model: preset.model,
model: resolveEnhanceProModel(config, preset.model),
upscaling_activated: preset.upscalingActivated,
image_scaling_factor: clampNumber(
preset.imageScalingFactor,
@ -250,8 +273,16 @@ export function EnhanceProDialog({
[configPayload?.config],
);
const presets = useMemo(
() => (config.presets.length ? config.presets : FALLBACK_CONFIG.presets),
[config.presets],
() => {
const availableModels = getAvailableEnhanceProModels(config);
return (config.presets.length ? config.presets : FALLBACK_CONFIG.presets)
.filter((preset) => availableModels.includes(preset.model));
},
[config],
);
const availableModels = useMemo(
() => getAvailableEnhanceProModels(config),
[config],
);
useEffect(() => {
@ -317,7 +348,7 @@ export function EnhanceProDialog({
face_enhancement_creativity: clampNumber(formState.face_enhancement_creativity, 0, 1),
face_enhancement_strength: clampNumber(formState.face_enhancement_strength, 0, 1),
crop_to_fill: formState.crop_to_fill,
model: formState.model,
model: resolveEnhanceProModel(config, formState.model),
upscaling_activated: formState.upscaling_activated,
image_scaling_factor: clampNumber(
formState.image_scaling_factor,
@ -519,10 +550,11 @@ export function EnhanceProDialog({
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Redefine">Redefine</SelectItem>
<SelectItem value="Recovery">Recovery</SelectItem>
<SelectItem value="Recovery V2">Recovery V2</SelectItem>
<SelectItem value="Reimagine">Reimagine</SelectItem>
{availableModels.map((model) => (
<SelectItem key={model} value={model}>
{model}
</SelectItem>
))}
</SelectContent>
</Select>
</div>

View File

@ -3537,6 +3537,61 @@ export async function consumeUserPoints({
}
}
export async function refundUserPoints({
userId,
amount,
reason = "points_refund",
modelId,
chatId,
messageId,
note,
}: {
userId: string;
amount: number;
reason?: string;
modelId?: string | null;
chatId?: string | null;
messageId?: string | null;
note?: string;
}) {
if (!Number.isFinite(amount) || amount <= 0) {
return { ok: true, balance: null as number | null, skipped: false };
}
try {
return await db.transaction(async (tx) => {
const updated = await tx
.update(user)
.set({
pointsBalance: sql`${user.pointsBalance} + ${amount}`,
updatedAt: new Date(),
})
.where(eq(user.id, userId))
.returning({
balance: user.pointsBalance,
});
if (updated.length === 0) {
return { ok: false, balance: null as number | null, skipped: false };
}
await tx.insert(pointsLedger).values({
userId,
amount,
reason,
modelId: modelId || null,
chatId: chatId || null,
messageId: messageId || null,
note: note || "失败任务退回积分",
});
return { ok: true, balance: updated[0]?.balance ?? null, skipped: false };
});
} catch (_error) {
throw new ChatSDKError("bad_request:database", "Failed to refund user points");
}
}
export async function getPointsLedgerByUserId({
userId,
limit = 100,

View File

@ -3,13 +3,15 @@ export const ENHANCE_PRO_KREA_DEFAULT_BASE_URL = "https://api.krea.ai";
export const ENHANCE_PRO_KREA_DEFAULT_PROVIDER_NAME = "Krea Topaz Generative";
export const ENHANCE_PRO_KREA_DEFAULT_MODEL = "Redefine";
export const ENHANCE_PRO_KREA_DEFAULT_OUTPUT_FORMAT = "jpg";
export const ENHANCE_PRO_KREA_MODELS = [
"Redefine",
"Recovery",
"Recovery V2",
"Reimagine",
] as const;
const TRAILING_SLASHES_REGEX = /\/+$/;
export type EnhanceProKreaModel =
| "Redefine"
| "Recovery"
| "Recovery V2"
| "Reimagine";
export type EnhanceProKreaModel = (typeof ENHANCE_PRO_KREA_MODELS)[number];
export type EnhanceProOutputFormat = "png" | "jpg" | "webp";
@ -46,6 +48,7 @@ export type EnhanceProKreaConfig = {
apiKey: string;
apiKeys: string[];
defaultModel: EnhanceProKreaModel;
hiddenModels: EnhanceProKreaModel[];
defaultOutputFormat: EnhanceProOutputFormat;
allowBatchSize: boolean;
maxBatchSize: number;
@ -114,6 +117,77 @@ function normalizeEnhanceProApiKeys(value: unknown) {
);
}
export function isEnhanceProKreaModel(
value: unknown,
): value is EnhanceProKreaModel {
const model = String(value || "").trim();
return ENHANCE_PRO_KREA_MODELS.includes(model as EnhanceProKreaModel);
}
function normalizeEnhanceProKreaModel(
value: unknown,
fallback: EnhanceProKreaModel,
) {
const model = String(value || "").trim();
return isEnhanceProKreaModel(model) ? model : fallback;
}
function normalizeEnhanceProHiddenModels(
value: unknown,
defaultModel: EnhanceProKreaModel,
) {
const candidates = Array.isArray(value)
? value
: typeof value === "string"
? value.split(/\r?\n|,/)
: [];
const hiddenModels = Array.from(
new Set(
candidates
.map((item) => String(item || "").trim())
.filter((item): item is EnhanceProKreaModel =>
isEnhanceProKreaModel(item),
),
),
);
if (hiddenModels.length >= ENHANCE_PRO_KREA_MODELS.length) {
return hiddenModels.filter((model) => model !== defaultModel);
}
return hiddenModels;
}
function ensureEnhanceProPresetDefault(presets: EnhanceProPreset[]) {
if (presets.length === 0) {
return presets;
}
if (presets.some((preset) => preset.isDefault)) {
return presets;
}
return presets.map((preset, index) => ({
...preset,
isDefault: index === 0,
}));
}
export function getVisibleEnhanceProKreaModels(
config: Pick<EnhanceProKreaConfig, "hiddenModels">,
) {
const hiddenModels = new Set(config.hiddenModels);
const visibleModels = ENHANCE_PRO_KREA_MODELS.filter(
(model) => !hiddenModels.has(model),
);
return visibleModels.length ? visibleModels : [...ENHANCE_PRO_KREA_MODELS];
}
export function isEnhanceProKreaModelHidden(
config: Pick<EnhanceProKreaConfig, "hiddenModels">,
model: EnhanceProKreaModel,
) {
return !getVisibleEnhanceProKreaModels(config).includes(model);
}
export function defaultEnhanceProPresets(): EnhanceProPreset[] {
return [
{
@ -261,13 +335,10 @@ function normalizeEnhanceProPreset(
),
cropToFill:
typeof raw.cropToFill === "boolean" ? raw.cropToFill : defaults.cropToFill,
model:
model === "Recovery" ||
model === "Recovery V2" ||
model === "Reimagine" ||
model === "Redefine"
? model
: defaults.model || configDefaults.defaultModel,
model: normalizeEnhanceProKreaModel(
model,
defaults.model || configDefaults.defaultModel,
),
upscalingActivated:
typeof raw.upscalingActivated === "boolean"
? raw.upscalingActivated
@ -294,6 +365,7 @@ export function defaultEnhanceProKreaConfig(): EnhanceProKreaConfig {
apiKey: "",
apiKeys: [],
defaultModel: ENHANCE_PRO_KREA_DEFAULT_MODEL,
hiddenModels: [],
defaultOutputFormat: ENHANCE_PRO_KREA_DEFAULT_OUTPUT_FORMAT,
allowBatchSize: true,
maxBatchSize: 4,
@ -314,7 +386,20 @@ export function normalizeEnhanceProKreaConfig(
? (value as Record<string, unknown>)
: {};
const defaultModel = String(raw.defaultModel || "").trim();
const rawDefaultModel = normalizeEnhanceProKreaModel(
raw.defaultModel,
defaults.defaultModel,
);
const hiddenModels = normalizeEnhanceProHiddenModels(
raw.hiddenModels || raw.disabledModels,
rawDefaultModel,
);
const visibleModels = ENHANCE_PRO_KREA_MODELS.filter(
(model) => !hiddenModels.includes(model),
);
const defaultModel = hiddenModels.includes(rawDefaultModel)
? visibleModels[0] || defaults.defaultModel
: rawDefaultModel;
const defaultOutputFormat = String(raw.defaultOutputFormat || "").trim();
const maxUpscaleFactor = clampFloat(
raw.maxUpscaleFactor,
@ -326,18 +411,13 @@ export function normalizeEnhanceProKreaConfig(
Array.isArray(raw.presets) ? raw.presets : defaultEnhanceProPresets()
).map((item, index) =>
normalizeEnhanceProPreset(item, index, {
defaultModel: defaults.defaultModel,
defaultModel,
defaultOutputFormat: defaults.defaultOutputFormat,
maxBatchSize: clampInteger(raw.maxBatchSize, 1, 4, defaults.maxBatchSize),
maxUpscaleFactor,
}),
);
const presetWithDefault = normalizedPresets.some((preset) => preset.isDefault)
? normalizedPresets
: normalizedPresets.map((preset, index) => ({
...preset,
isDefault: index === 0,
}));
const presetWithDefault = ensureEnhanceProPresetDefault(normalizedPresets);
return {
enabled: raw.enabled === true,
@ -351,13 +431,8 @@ export function normalizeEnhanceProKreaConfig(
normalizeEnhanceProApiKey(raw.apiKey) ||
normalizeEnhanceProApiKey(apiKeyFallback),
apiKeys: normalizeEnhanceProApiKeys(raw.apiKeys || raw.apiKeyPool),
defaultModel:
defaultModel === "Recovery" ||
defaultModel === "Recovery V2" ||
defaultModel === "Reimagine" ||
defaultModel === "Redefine"
? defaultModel
: defaults.defaultModel,
defaultModel,
hiddenModels,
defaultOutputFormat:
defaultOutputFormat === "png" ||
defaultOutputFormat === "webp" ||
@ -381,18 +456,33 @@ export function normalizeEnhanceProKreaConfig(
export function sanitizeEnhanceProKreaConfigForClient(
config: EnhanceProKreaConfig,
) {
const availableModels = getVisibleEnhanceProKreaModels(config);
const defaultModel = availableModels.includes(config.defaultModel)
? config.defaultModel
: availableModels[0] || ENHANCE_PRO_KREA_DEFAULT_MODEL;
const visiblePresets = config.presets.filter((preset) =>
availableModels.includes(preset.model),
);
const fallbackPresets = defaultEnhanceProPresets().filter((preset) =>
availableModels.includes(preset.model),
);
const presets = ensureEnhanceProPresetDefault(
visiblePresets.length ? visiblePresets : fallbackPresets,
);
return {
enabled: config.enabled,
providerName: config.providerName,
baseUrl: config.baseUrl,
keyPoolSize: config.apiKeys.length,
defaultModel: config.defaultModel,
defaultModel,
availableModels,
defaultOutputFormat: config.defaultOutputFormat,
allowBatchSize: config.allowBatchSize,
maxBatchSize: config.maxBatchSize,
maxUpscaleFactor: config.maxUpscaleFactor,
maxOutputEdge: config.maxOutputEdge,
enableAdvancedControls: config.enableAdvancedControls,
presets: config.presets,
presets,
};
}

File diff suppressed because one or more lines are too long