feat: update enhance pro configuration
This commit is contained in:
parent
b8e8b45b6e
commit
66fd760b9a
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 "积分充值";
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user