fix: harden enhance pro task charging
This commit is contained in:
parent
9f17020582
commit
b8a59fe2a4
@ -1,4 +1,5 @@
|
|||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
|
import { and, eq, or } from "drizzle-orm";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { auth } from "@/app/(auth)/auth";
|
import { auth } from "@/app/(auth)/auth";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
@ -11,7 +12,11 @@ import {
|
|||||||
getUserById,
|
getUserById,
|
||||||
upsertMediaAsset,
|
upsertMediaAsset,
|
||||||
} from "@/lib/db/queries";
|
} from "@/lib/db/queries";
|
||||||
import { generatedImage } from "@/lib/db/schema";
|
import {
|
||||||
|
enhanceProTask,
|
||||||
|
generatedImage,
|
||||||
|
pointsLedger,
|
||||||
|
} from "@/lib/db/schema";
|
||||||
import {
|
import {
|
||||||
ENHANCE_PRO_KREA_SETTING_KEY,
|
ENHANCE_PRO_KREA_SETTING_KEY,
|
||||||
normalizeEnhanceProKreaConfig,
|
normalizeEnhanceProKreaConfig,
|
||||||
@ -24,7 +29,6 @@ import { calculateEnhanceProPointsEstimate } from "@/lib/points/config";
|
|||||||
import { uploadFile } from "@/lib/storage/service";
|
import { uploadFile } from "@/lib/storage/service";
|
||||||
|
|
||||||
const ENHANCE_PRO_CREATE_TIMEOUT_MS = 120_000;
|
const ENHANCE_PRO_CREATE_TIMEOUT_MS = 120_000;
|
||||||
const ENHANCE_PRO_SYNC_WAIT_TIMEOUT_MS = 20_000;
|
|
||||||
const ENHANCE_PRO_TASK_POLL_INTERVAL_MS = 1_500;
|
const ENHANCE_PRO_TASK_POLL_INTERVAL_MS = 1_500;
|
||||||
const ENHANCE_PRO_MODEL_ID = "krea-topaz-generative";
|
const ENHANCE_PRO_MODEL_ID = "krea-topaz-generative";
|
||||||
const ENHANCE_PRO_MAX_RESULT_IMAGES = 4;
|
const ENHANCE_PRO_MAX_RESULT_IMAGES = 4;
|
||||||
@ -37,6 +41,7 @@ type EnhanceProRequestBody = {
|
|||||||
prompt?: string;
|
prompt?: string;
|
||||||
chatId?: string | null;
|
chatId?: string | null;
|
||||||
messageId?: string | null;
|
messageId?: string | null;
|
||||||
|
clientRequestId?: string | null;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
batchSize?: number;
|
batchSize?: number;
|
||||||
@ -64,6 +69,27 @@ type FinalizedImageResult = {
|
|||||||
height?: number;
|
height?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type EnhanceProStoredRequest = {
|
||||||
|
prompt: string;
|
||||||
|
sourceImageUrl: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
batchSize: number;
|
||||||
|
model: EnhanceProKreaModel;
|
||||||
|
upscalingActivated: boolean;
|
||||||
|
imageScalingFactor: number;
|
||||||
|
chatId: string | null;
|
||||||
|
messageId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EnhanceProSuccessPayload = Awaited<
|
||||||
|
ReturnType<typeof finalizeEnhanceProTaskResult>
|
||||||
|
> & {
|
||||||
|
chargedPoints: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EnhanceProTaskRecord = typeof enhanceProTask.$inferSelect;
|
||||||
|
|
||||||
function clampNumber(value: unknown, min: number, max: number, fallback: number) {
|
function clampNumber(value: unknown, min: number, max: number, fallback: number) {
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
if (!Number.isFinite(parsed)) return fallback;
|
if (!Number.isFinite(parsed)) return fallback;
|
||||||
@ -85,6 +111,10 @@ function normalizeJobStatus(value: unknown) {
|
|||||||
return String(value || "").trim().toLowerCase();
|
return String(value || "").trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeClientRequestId(value: unknown) {
|
||||||
|
return String(value || "").trim().slice(0, 64);
|
||||||
|
}
|
||||||
|
|
||||||
function getEnhanceProKeyFingerprint(apiKey: string) {
|
function getEnhanceProKeyFingerprint(apiKey: string) {
|
||||||
return createHash("sha256")
|
return createHash("sha256")
|
||||||
.update(String(apiKey || "").trim())
|
.update(String(apiKey || "").trim())
|
||||||
@ -126,14 +156,13 @@ function resolveEnhanceProApiKey(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEnhanceProTaskPendingResponse(taskId: string, taskKeyId?: string | null) {
|
function buildEnhanceProTaskPendingResponse(taskId: string) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
success: true,
|
success: true,
|
||||||
processing: true,
|
processing: true,
|
||||||
status: "running",
|
status: "running",
|
||||||
taskId,
|
taskId,
|
||||||
taskKeyId,
|
|
||||||
pollAfterMs: ENHANCE_PRO_TASK_POLL_INTERVAL_MS,
|
pollAfterMs: ENHANCE_PRO_TASK_POLL_INTERVAL_MS,
|
||||||
},
|
},
|
||||||
{ status: 202 }
|
{ status: 202 }
|
||||||
@ -252,7 +281,8 @@ async function resolveSourceInput(body: EnhanceProRequestBody) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (imageUrl) {
|
if (imageUrl) {
|
||||||
const hasRequestedDimensions = Number.isFinite(Number(body.width)) && Number.isFinite(Number(body.height));
|
const hasRequestedDimensions =
|
||||||
|
Number.isFinite(Number(body.width)) && Number.isFinite(Number(body.height));
|
||||||
if (hasRequestedDimensions) {
|
if (hasRequestedDimensions) {
|
||||||
return {
|
return {
|
||||||
sourceImageUrl: imageUrl,
|
sourceImageUrl: imageUrl,
|
||||||
@ -351,12 +381,7 @@ async function createEnhanceProJob(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pollEnhanceProJob(
|
async function fetchEnhanceProJobStatus(config: EnhanceProKreaConfig, jobId: string) {
|
||||||
config: EnhanceProKreaConfig,
|
|
||||||
jobId: string,
|
|
||||||
delay = ENHANCE_PRO_TASK_POLL_INTERVAL_MS,
|
|
||||||
timeout = ENHANCE_PRO_SYNC_WAIT_TIMEOUT_MS,
|
|
||||||
): Promise<any> {
|
|
||||||
const apiUrl = normalizeBaseUrl(config.baseUrl);
|
const apiUrl = normalizeBaseUrl(config.baseUrl);
|
||||||
const response = await fetch(`${apiUrl}/jobs/${jobId}`, {
|
const response = await fetch(`${apiUrl}/jobs/${jobId}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@ -371,27 +396,7 @@ async function pollEnhanceProJob(
|
|||||||
throw new Error(`查询任务状态失败: ${response.status} ${detail || response.statusText}`);
|
throw new Error(`查询任务状态失败: ${response.status} ${detail || response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = await response.json();
|
return await response.json();
|
||||||
const status = normalizeJobStatus(payload?.status);
|
|
||||||
|
|
||||||
if (status === "completed") {
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === "failed" || status === "cancelled") {
|
|
||||||
throw new Error(`Krea 任务${status === "failed" ? "失败" : "已取消"}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeout <= 0) {
|
|
||||||
throw new Error("轮询超时");
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
||||||
return pollEnhanceProJob(config, jobId, delay, timeout - delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isEnhanceProTaskStillProcessing(error: unknown) {
|
|
||||||
return error instanceof Error && /轮询超时/i.test(error.message || "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveGeneratedImageRecord(params: {
|
async function saveGeneratedImageRecord(params: {
|
||||||
@ -534,6 +539,235 @@ async function finalizeEnhanceProTaskResult(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseEnhanceProStoredRequest(task: EnhanceProTaskRecord): EnhanceProStoredRequest {
|
||||||
|
const request =
|
||||||
|
task.request && typeof task.request === "object"
|
||||||
|
? (task.request as Partial<EnhanceProStoredRequest>)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const width = sanitizeDimension(request.width, 1024);
|
||||||
|
const height = sanitizeDimension(request.height, 1024);
|
||||||
|
|
||||||
|
return {
|
||||||
|
prompt: String(request.prompt || task.prompt || "").trim() || "图像增强 Pro",
|
||||||
|
sourceImageUrl:
|
||||||
|
String(request.sourceImageUrl || task.sourceImageUrl || "").trim(),
|
||||||
|
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",
|
||||||
|
upscalingActivated: normalizeBoolean(request.upscalingActivated, false),
|
||||||
|
imageScalingFactor: clampNumber(request.imageScalingFactor, 1, 8, 1),
|
||||||
|
chatId: request.chatId ? String(request.chatId) : task.chatId || null,
|
||||||
|
messageId: request.messageId ? String(request.messageId) : task.messageId || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEnhanceProTaskForUser(params: { taskId: string; userId: string }) {
|
||||||
|
const rows = await db
|
||||||
|
.select()
|
||||||
|
.from(enhanceProTask)
|
||||||
|
.where(and(eq(enhanceProTask.id, params.taskId), eq(enhanceProTask.userId, params.userId)))
|
||||||
|
.limit(1);
|
||||||
|
return rows[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEnhanceProTaskByClientRequestId(params: {
|
||||||
|
userId: string;
|
||||||
|
clientRequestId: string;
|
||||||
|
}) {
|
||||||
|
const rows = await db
|
||||||
|
.select()
|
||||||
|
.from(enhanceProTask)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(enhanceProTask.userId, params.userId),
|
||||||
|
eq(enhanceProTask.clientRequestId, params.clientRequestId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
return rows[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markEnhanceProTaskRunning(taskId: string) {
|
||||||
|
await db
|
||||||
|
.update(enhanceProTask)
|
||||||
|
.set({
|
||||||
|
status: "running",
|
||||||
|
updatedAt: new Date(),
|
||||||
|
error: null,
|
||||||
|
})
|
||||||
|
.where(eq(enhanceProTask.id, taskId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function claimEnhanceProTaskFinalization(taskId: string) {
|
||||||
|
const rows = await db
|
||||||
|
.update(enhanceProTask)
|
||||||
|
.set({
|
||||||
|
status: "finalizing",
|
||||||
|
updatedAt: new Date(),
|
||||||
|
error: null,
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(enhanceProTask.id, taskId),
|
||||||
|
or(eq(enhanceProTask.status, "pending"), eq(enhanceProTask.status, "running")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
return rows[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markEnhanceProTaskFailed(taskId: string, errorMessage: string) {
|
||||||
|
await db
|
||||||
|
.update(enhanceProTask)
|
||||||
|
.set({
|
||||||
|
status: "failed",
|
||||||
|
error: errorMessage,
|
||||||
|
completedAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(enhanceProTask.id, taskId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistEnhanceProTaskSuccess(params: {
|
||||||
|
taskId: string;
|
||||||
|
payload: EnhanceProSuccessPayload;
|
||||||
|
pointsCharged: boolean;
|
||||||
|
}) {
|
||||||
|
await db
|
||||||
|
.update(enhanceProTask)
|
||||||
|
.set({
|
||||||
|
status: "succeeded",
|
||||||
|
result: params.payload as Record<string, unknown>,
|
||||||
|
error: null,
|
||||||
|
pointsCharged: params.pointsCharged,
|
||||||
|
chargedAt: params.pointsCharged ? new Date() : null,
|
||||||
|
completedAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(enhanceProTask.id, params.taskId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function consumeEnhanceProPointsOnce(
|
||||||
|
task: EnhanceProTaskRecord,
|
||||||
|
request: EnhanceProStoredRequest,
|
||||||
|
) {
|
||||||
|
if (task.pointsCost <= 0) {
|
||||||
|
return { ok: true, skipped: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const note = `图像增强 Pro 消耗积分(任务 ${task.id} / ${task.providerTaskId})`;
|
||||||
|
const existingLedger = await db
|
||||||
|
.select({ id: pointsLedger.id })
|
||||||
|
.from(pointsLedger)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(pointsLedger.userId, task.userId),
|
||||||
|
eq(pointsLedger.reason, "enhance_pro_generation"),
|
||||||
|
eq(pointsLedger.note, note),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existingLedger[0]) {
|
||||||
|
return { ok: true, skipped: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return await consumeUserPoints({
|
||||||
|
userId: task.userId,
|
||||||
|
amount: task.pointsCost,
|
||||||
|
reason: "enhance_pro_generation",
|
||||||
|
modelId: `${ENHANCE_PRO_MODEL_ID}:${request.model}`,
|
||||||
|
chatId: request.chatId,
|
||||||
|
messageId: request.messageId,
|
||||||
|
note,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStoredEnhanceProSuccessPayload(task: EnhanceProTaskRecord) {
|
||||||
|
if (!task.result || typeof task.result !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return task.result as EnhanceProSuccessPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function finalizeEnhanceProTask(
|
||||||
|
task: EnhanceProTaskRecord,
|
||||||
|
taskResult: any,
|
||||||
|
startedAt?: number,
|
||||||
|
) {
|
||||||
|
const existingPayload = getStoredEnhanceProSuccessPayload(task);
|
||||||
|
if (task.status === "succeeded" && existingPayload) {
|
||||||
|
return existingPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
const claimedTask =
|
||||||
|
task.status === "finalizing" ? task : await claimEnhanceProTaskFinalization(task.id);
|
||||||
|
if (!claimedTask) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = parseEnhanceProStoredRequest(claimedTask);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const responsePayload = await finalizeEnhanceProTaskResult({
|
||||||
|
userId: claimedTask.userId,
|
||||||
|
taskResult,
|
||||||
|
prompt: request.prompt,
|
||||||
|
chatId: request.chatId,
|
||||||
|
messageId: request.messageId,
|
||||||
|
startedAt,
|
||||||
|
sourceImageUrls: request.sourceImageUrl ? [request.sourceImageUrl] : undefined,
|
||||||
|
width: request.width,
|
||||||
|
height: request.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
let pointsCharged = claimedTask.pointsCharged;
|
||||||
|
if (!pointsCharged && claimedTask.pointsCost > 0) {
|
||||||
|
try {
|
||||||
|
const chargeResult = await consumeEnhanceProPointsOnce(claimedTask, request);
|
||||||
|
pointsCharged = chargeResult.ok || chargeResult.skipped;
|
||||||
|
} catch (pointsError) {
|
||||||
|
console.error("[Points] Failed to consume Enhance Pro points:", pointsError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: EnhanceProSuccessPayload = {
|
||||||
|
...responsePayload,
|
||||||
|
chargedPoints: pointsCharged ? claimedTask.pointsCost : 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
await persistEnhanceProTaskSuccess({
|
||||||
|
taskId: claimedTask.id,
|
||||||
|
payload,
|
||||||
|
pointsCharged,
|
||||||
|
});
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "未知错误";
|
||||||
|
await markEnhanceProTaskFailed(claimedTask.id, errorMessage);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEnhanceProTaskFailedResponse(task: EnhanceProTaskRecord) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: "图像增强 Pro 处理失败",
|
||||||
|
detail: String(task.error || "任务失败"),
|
||||||
|
},
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadEnhanceProConfig() {
|
async function loadEnhanceProConfig() {
|
||||||
const setting = await getSystemSetting({ key: ENHANCE_PRO_KREA_SETTING_KEY });
|
const setting = await getSystemSetting({ key: ENHANCE_PRO_KREA_SETTING_KEY });
|
||||||
const existingValue =
|
const existingValue =
|
||||||
@ -558,50 +792,76 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: "缺少 taskId 参数" }, { status: 400 });
|
return NextResponse.json({ error: "缺少 taskId 参数" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const task = await getEnhanceProTaskForUser({
|
||||||
|
taskId,
|
||||||
|
userId: session.user.id,
|
||||||
|
});
|
||||||
|
if (!task) {
|
||||||
|
return NextResponse.json({ error: "任务不存在" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedPayload = getStoredEnhanceProSuccessPayload(task);
|
||||||
|
if (task.status === "succeeded" && storedPayload) {
|
||||||
|
return NextResponse.json(storedPayload);
|
||||||
|
}
|
||||||
|
if (task.status === "failed") {
|
||||||
|
return buildEnhanceProTaskFailedResponse(task);
|
||||||
|
}
|
||||||
|
if (task.status === "finalizing") {
|
||||||
|
return buildEnhanceProTaskPendingResponse(task.id);
|
||||||
|
}
|
||||||
|
|
||||||
const config = await loadEnhanceProConfig();
|
const config = await loadEnhanceProConfig();
|
||||||
if (!config.enabled) {
|
if (!config.enabled) {
|
||||||
return NextResponse.json({ error: "图像增强 Pro 功能未开启" }, { status: 403 });
|
return NextResponse.json({ error: "图像增强 Pro 功能未开启" }, { status: 403 });
|
||||||
}
|
}
|
||||||
const keySelection = resolveEnhanceProApiKey(config, searchParams.get("taskKeyId"));
|
const keySelection = resolveEnhanceProApiKey(config, task.taskKeyId);
|
||||||
if (!keySelection || !config.baseUrl) {
|
if (!keySelection || !config.baseUrl) {
|
||||||
return NextResponse.json({ error: "图像增强 Pro API 尚未配置" }, { status: 400 });
|
return NextResponse.json({ error: "图像增强 Pro API 尚未配置" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
let taskResult: any;
|
const taskResult = await fetchEnhanceProJobStatus(
|
||||||
try {
|
{ ...config, apiKey: keySelection.apiKey },
|
||||||
taskResult = await pollEnhanceProJob(
|
task.providerTaskId,
|
||||||
{ ...config, apiKey: keySelection.apiKey },
|
);
|
||||||
taskId
|
const providerStatus = normalizeJobStatus(taskResult?.status);
|
||||||
);
|
|
||||||
} catch (pollError) {
|
if (providerStatus === "completed") {
|
||||||
if (isEnhanceProTaskStillProcessing(pollError)) {
|
const completedPayload = await finalizeEnhanceProTask(task, taskResult);
|
||||||
return buildEnhanceProTaskPendingResponse(taskId, keySelection.keyId);
|
if (completedPayload) {
|
||||||
|
return NextResponse.json(completedPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshedTask = await getEnhanceProTaskForUser({
|
||||||
|
taskId,
|
||||||
|
userId: session.user.id,
|
||||||
|
});
|
||||||
|
const refreshedPayload = refreshedTask
|
||||||
|
? getStoredEnhanceProSuccessPayload(refreshedTask)
|
||||||
|
: null;
|
||||||
|
if (refreshedTask?.status === "succeeded" && refreshedPayload) {
|
||||||
|
return NextResponse.json(refreshedPayload);
|
||||||
|
}
|
||||||
|
if (refreshedTask?.status === "failed") {
|
||||||
|
return buildEnhanceProTaskFailedResponse(refreshedTask);
|
||||||
|
}
|
||||||
|
return buildEnhanceProTaskPendingResponse(task.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (providerStatus === "failed" || providerStatus === "cancelled") {
|
||||||
|
const detail = `Krea 任务${providerStatus === "failed" ? "失败" : "已取消"}`;
|
||||||
|
await markEnhanceProTaskFailed(task.id, detail);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: "图像增强 Pro 处理失败",
|
error: "图像增强 Pro 处理失败",
|
||||||
detail: pollError instanceof Error ? pollError.message : "轮询失败",
|
detail,
|
||||||
},
|
},
|
||||||
{ status: 500 },
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const width = sanitizeDimension(searchParams.get("width"), 1024);
|
await markEnhanceProTaskRunning(task.id);
|
||||||
const height = sanitizeDimension(searchParams.get("height"), 1024);
|
return buildEnhanceProTaskPendingResponse(task.id);
|
||||||
const responsePayload = await finalizeEnhanceProTaskResult({
|
|
||||||
userId: session.user.id,
|
|
||||||
taskResult,
|
|
||||||
prompt: searchParams.get("prompt") || "图像增强 Pro",
|
|
||||||
chatId: searchParams.get("chatId"),
|
|
||||||
messageId: searchParams.get("messageId"),
|
|
||||||
sourceImageUrls: searchParams.get("sourceImageUrl")
|
|
||||||
? [String(searchParams.get("sourceImageUrl"))]
|
|
||||||
: undefined,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json(responsePayload);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Enhance Pro API][TaskStatus] 错误:", error);
|
console.error("[Enhance Pro API][TaskStatus] 错误:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@ -629,6 +889,24 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestConfig = { ...config, apiKey: keySelection.apiKey };
|
const requestConfig = { ...config, apiKey: keySelection.apiKey };
|
||||||
|
|
||||||
const body = (await request.json()) as EnhanceProRequestBody;
|
const body = (await request.json()) as EnhanceProRequestBody;
|
||||||
|
const clientRequestId = normalizeClientRequestId(body.clientRequestId);
|
||||||
|
if (clientRequestId) {
|
||||||
|
const existingTask = await getEnhanceProTaskByClientRequestId({
|
||||||
|
userId: session.user.id,
|
||||||
|
clientRequestId,
|
||||||
|
});
|
||||||
|
if (existingTask) {
|
||||||
|
const existingPayload = getStoredEnhanceProSuccessPayload(existingTask);
|
||||||
|
if (existingTask.status === "succeeded" && existingPayload) {
|
||||||
|
return NextResponse.json(existingPayload);
|
||||||
|
}
|
||||||
|
if (existingTask.status === "failed") {
|
||||||
|
return buildEnhanceProTaskFailedResponse(existingTask);
|
||||||
|
}
|
||||||
|
return buildEnhanceProTaskPendingResponse(existingTask.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const prompt = String(body.prompt || "").trim();
|
const prompt = String(body.prompt || "").trim();
|
||||||
const chatId = body.chatId || null;
|
const chatId = body.chatId || null;
|
||||||
const messageId = body.messageId || null;
|
const messageId = body.messageId || null;
|
||||||
@ -706,78 +984,100 @@ export async function POST(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const createdStatus = normalizeJobStatus(createdJob?.status);
|
const createdStatus = normalizeJobStatus(createdJob?.status);
|
||||||
const taskId = String(createdJob?.job_id || "").trim();
|
const providerTaskId = String(createdJob?.job_id || "").trim();
|
||||||
if (!taskId) {
|
if (!providerTaskId) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "创建图像增强 Pro 任务失败", detail: "Krea 未返回 job_id" },
|
{ error: "创建图像增强 Pro 任务失败", detail: "Krea 未返回 job_id" },
|
||||||
{ status: 500 },
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pointsEstimate.chargeable) {
|
const storedRequest: EnhanceProStoredRequest = {
|
||||||
try {
|
prompt: prompt || "图像增强 Pro",
|
||||||
await consumeUserPoints({
|
sourceImageUrl: sourceInput.sourceImageUrl,
|
||||||
|
width: clampedDimensions.width,
|
||||||
|
height: clampedDimensions.height,
|
||||||
|
batchSize,
|
||||||
|
model: selectedModel,
|
||||||
|
upscalingActivated,
|
||||||
|
imageScalingFactor,
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
};
|
||||||
|
|
||||||
|
let createdTask: EnhanceProTaskRecord | null = null;
|
||||||
|
try {
|
||||||
|
const inserted = await db
|
||||||
|
.insert(enhanceProTask)
|
||||||
|
.values({
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
amount: pointsEstimate.cost,
|
|
||||||
reason: "enhance_pro_generation",
|
|
||||||
modelId: `${ENHANCE_PRO_MODEL_ID}:${selectedModel}`,
|
|
||||||
chatId,
|
chatId,
|
||||||
messageId,
|
messageId,
|
||||||
note: "图像增强 Pro 消耗积分",
|
clientRequestId: clientRequestId || null,
|
||||||
|
providerTaskId,
|
||||||
|
taskKeyId: keySelection.keyId,
|
||||||
|
status:
|
||||||
|
createdStatus === "completed"
|
||||||
|
? "running"
|
||||||
|
: createdStatus === "failed" || createdStatus === "cancelled"
|
||||||
|
? "failed"
|
||||||
|
: "pending",
|
||||||
|
prompt: storedRequest.prompt,
|
||||||
|
sourceImageUrl: storedRequest.sourceImageUrl,
|
||||||
|
request: storedRequest as unknown as Record<string, unknown>,
|
||||||
|
error:
|
||||||
|
createdStatus === "failed" || createdStatus === "cancelled"
|
||||||
|
? `Krea 任务${createdStatus === "failed" ? "失败" : "已取消"}`
|
||||||
|
: null,
|
||||||
|
pointsCost: pointsEstimate.chargeable ? pointsEstimate.cost : 0,
|
||||||
|
pointsCharged: false,
|
||||||
|
completedAt:
|
||||||
|
createdStatus === "failed" || createdStatus === "cancelled" ? new Date() : null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
createdTask = inserted[0] ?? null;
|
||||||
|
} catch (insertError) {
|
||||||
|
if (clientRequestId) {
|
||||||
|
const existingTask = await getEnhanceProTaskByClientRequestId({
|
||||||
|
userId: session.user.id,
|
||||||
|
clientRequestId,
|
||||||
});
|
});
|
||||||
} catch (pointsError) {
|
if (existingTask) {
|
||||||
console.error("[Points] Failed to consume Enhance Pro points:", pointsError);
|
const existingPayload = getStoredEnhanceProSuccessPayload(existingTask);
|
||||||
|
if (existingTask.status === "succeeded" && existingPayload) {
|
||||||
|
return NextResponse.json(existingPayload);
|
||||||
|
}
|
||||||
|
if (existingTask.status === "failed") {
|
||||||
|
return buildEnhanceProTaskFailedResponse(existingTask);
|
||||||
|
}
|
||||||
|
return buildEnhanceProTaskPendingResponse(existingTask.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
throw insertError;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (createdStatus === "completed") {
|
if (!createdTask) {
|
||||||
const responsePayload = await finalizeEnhanceProTaskResult({
|
|
||||||
userId: session.user.id,
|
|
||||||
taskResult: createdJob,
|
|
||||||
prompt,
|
|
||||||
chatId,
|
|
||||||
messageId,
|
|
||||||
startedAt: startTime,
|
|
||||||
sourceImageUrls: [sourceInput.sourceImageUrl],
|
|
||||||
width: clampedDimensions.width,
|
|
||||||
height: clampedDimensions.height,
|
|
||||||
});
|
|
||||||
return NextResponse.json({
|
|
||||||
...responsePayload,
|
|
||||||
chargedPoints: pointsEstimate.chargeable ? pointsEstimate.cost : 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const taskResult = await pollEnhanceProJob(requestConfig, taskId);
|
|
||||||
const responsePayload = await finalizeEnhanceProTaskResult({
|
|
||||||
userId: session.user.id,
|
|
||||||
taskResult,
|
|
||||||
prompt,
|
|
||||||
chatId,
|
|
||||||
messageId,
|
|
||||||
startedAt: startTime,
|
|
||||||
sourceImageUrls: [sourceInput.sourceImageUrl],
|
|
||||||
width: clampedDimensions.width,
|
|
||||||
height: clampedDimensions.height,
|
|
||||||
});
|
|
||||||
return NextResponse.json({
|
|
||||||
...responsePayload,
|
|
||||||
chargedPoints: pointsEstimate.chargeable ? pointsEstimate.cost : 0,
|
|
||||||
});
|
|
||||||
} catch (pollError) {
|
|
||||||
if (isEnhanceProTaskStillProcessing(pollError)) {
|
|
||||||
return buildEnhanceProTaskPendingResponse(taskId, keySelection.keyId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{ error: "创建图像增强 Pro 任务失败", detail: "任务入库失败" },
|
||||||
error: "图像增强 Pro 处理失败",
|
|
||||||
detail: pollError instanceof Error ? pollError.message : "轮询失败",
|
|
||||||
},
|
|
||||||
{ status: 500 },
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (createdStatus === "completed") {
|
||||||
|
const responsePayload = await finalizeEnhanceProTask(createdTask, createdJob, startTime);
|
||||||
|
if (responsePayload) {
|
||||||
|
return NextResponse.json(responsePayload);
|
||||||
|
}
|
||||||
|
return buildEnhanceProTaskPendingResponse(createdTask.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createdStatus === "failed" || createdStatus === "cancelled") {
|
||||||
|
return buildEnhanceProTaskFailedResponse(createdTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildEnhanceProTaskPendingResponse(createdTask.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Enhance Pro API] 错误:", error);
|
console.error("[Enhance Pro API] 错误:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@ -60,6 +60,46 @@ import {
|
|||||||
|
|
||||||
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
function createEnhanceProClientRequestId() {
|
||||||
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}
|
||||||
|
return `enhance-pro-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldPreserveEnhanceProRequestId(message: string) {
|
||||||
|
return /任务.*已提交|避免重复提交|稍后重试查看结果|failed to fetch|network/i.test(
|
||||||
|
String(message || "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEnhanceProRequestSignature(params: EnhanceProRequestParams) {
|
||||||
|
return JSON.stringify({
|
||||||
|
imageUrl: params.imageUrl || null,
|
||||||
|
image_base64: params.image_base64 || null,
|
||||||
|
mimeType: params.mimeType || null,
|
||||||
|
prompt: params.prompt || "",
|
||||||
|
width: params.width,
|
||||||
|
height: params.height,
|
||||||
|
batchSize: params.batchSize || 1,
|
||||||
|
seed: params.seed || 0,
|
||||||
|
output_format: params.output_format || null,
|
||||||
|
subject_detection: params.subject_detection || null,
|
||||||
|
face_enhancement: params.face_enhancement === true,
|
||||||
|
face_enhancement_creativity: params.face_enhancement_creativity ?? null,
|
||||||
|
face_enhancement_strength: params.face_enhancement_strength ?? null,
|
||||||
|
crop_to_fill: params.crop_to_fill === true,
|
||||||
|
model: params.model || null,
|
||||||
|
upscaling_activated: params.upscaling_activated === true,
|
||||||
|
image_scaling_factor: params.image_scaling_factor ?? 1,
|
||||||
|
creativity: params.creativity ?? null,
|
||||||
|
texture: params.texture ?? null,
|
||||||
|
sharpen: params.sharpen ?? null,
|
||||||
|
denoise: params.denoise ?? null,
|
||||||
|
detail: params.detail ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
type EnhanceProClientConfig = {
|
type EnhanceProClientConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
providerName: string;
|
providerName: string;
|
||||||
@ -398,6 +438,8 @@ export function EnhanceProWorkspace({
|
|||||||
const [faceOpen, setFaceOpen] = useState(true);
|
const [faceOpen, setFaceOpen] = useState(true);
|
||||||
const [comparePosition, setComparePosition] = useState(50);
|
const [comparePosition, setComparePosition] = useState(50);
|
||||||
const compareDragPointerIdRef = useRef<number | null>(null);
|
const compareDragPointerIdRef = useRef<number | null>(null);
|
||||||
|
const requestIdRef = useRef<string | null>(null);
|
||||||
|
const requestSignatureRef = useRef<string | null>(null);
|
||||||
|
|
||||||
const { data: configPayload, isLoading } = useSWR<{
|
const { data: configPayload, isLoading } = useSWR<{
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
@ -552,50 +594,61 @@ export function EnhanceProWorkspace({
|
|||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
setLastErrorMessage("");
|
setLastErrorMessage("");
|
||||||
|
const requestParams: EnhanceProRequestParams = {
|
||||||
|
...sourcePayload,
|
||||||
|
prompt: formState.prompt.trim() || "图像增强 Pro",
|
||||||
|
width: clampNumber(
|
||||||
|
Math.round(formState.width),
|
||||||
|
1,
|
||||||
|
config.maxOutputEdge
|
||||||
|
),
|
||||||
|
height: clampNumber(
|
||||||
|
Math.round(formState.height),
|
||||||
|
1,
|
||||||
|
config.maxOutputEdge
|
||||||
|
),
|
||||||
|
batchSize: config.allowBatchSize
|
||||||
|
? clampNumber(Math.round(formState.batchSize), 1, config.maxBatchSize)
|
||||||
|
: 1,
|
||||||
|
seed: Math.round(formState.seed),
|
||||||
|
output_format: formState.output_format,
|
||||||
|
subject_detection: formState.subject_detection,
|
||||||
|
face_enhancement: formState.face_enhancement,
|
||||||
|
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,
|
||||||
|
upscaling_activated: formState.upscaling_activated,
|
||||||
|
image_scaling_factor: clampNumber(
|
||||||
|
formState.image_scaling_factor,
|
||||||
|
1,
|
||||||
|
config.maxUpscaleFactor
|
||||||
|
),
|
||||||
|
creativity: clampNumber(Math.round(formState.creativity), 1, 6),
|
||||||
|
texture: clampNumber(Math.round(formState.texture), 1, 5),
|
||||||
|
sharpen: clampNumber(formState.sharpen, 0, 1),
|
||||||
|
denoise: clampNumber(formState.denoise, 0, 1),
|
||||||
|
detail: clampNumber(formState.detail, 0, 1),
|
||||||
|
};
|
||||||
|
const requestSignature = buildEnhanceProRequestSignature(requestParams);
|
||||||
|
const clientRequestId =
|
||||||
|
requestIdRef.current && requestSignatureRef.current === requestSignature
|
||||||
|
? requestIdRef.current
|
||||||
|
: createEnhanceProClientRequestId();
|
||||||
|
requestIdRef.current = clientRequestId;
|
||||||
|
requestSignatureRef.current = requestSignature;
|
||||||
try {
|
try {
|
||||||
const result = await imageGenerationService.enhanceImagePro({
|
const result = await imageGenerationService.enhanceImagePro({
|
||||||
...sourcePayload,
|
...requestParams,
|
||||||
prompt: formState.prompt.trim() || "图像增强 Pro",
|
clientRequestId,
|
||||||
width: clampNumber(
|
|
||||||
Math.round(formState.width),
|
|
||||||
1,
|
|
||||||
config.maxOutputEdge
|
|
||||||
),
|
|
||||||
height: clampNumber(
|
|
||||||
Math.round(formState.height),
|
|
||||||
1,
|
|
||||||
config.maxOutputEdge
|
|
||||||
),
|
|
||||||
batchSize: config.allowBatchSize
|
|
||||||
? clampNumber(Math.round(formState.batchSize), 1, config.maxBatchSize)
|
|
||||||
: 1,
|
|
||||||
seed: Math.round(formState.seed),
|
|
||||||
output_format: formState.output_format,
|
|
||||||
subject_detection: formState.subject_detection,
|
|
||||||
face_enhancement: formState.face_enhancement,
|
|
||||||
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,
|
|
||||||
upscaling_activated: formState.upscaling_activated,
|
|
||||||
image_scaling_factor: clampNumber(
|
|
||||||
formState.image_scaling_factor,
|
|
||||||
1,
|
|
||||||
config.maxUpscaleFactor
|
|
||||||
),
|
|
||||||
creativity: clampNumber(Math.round(formState.creativity), 1, 6),
|
|
||||||
texture: clampNumber(Math.round(formState.texture), 1, 5),
|
|
||||||
sharpen: clampNumber(formState.sharpen, 0, 1),
|
|
||||||
denoise: clampNumber(formState.denoise, 0, 1),
|
|
||||||
detail: clampNumber(formState.detail, 0, 1),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.success || !result.data?.images?.length) {
|
if (!result.success || !result.data?.images?.length) {
|
||||||
@ -604,12 +657,18 @@ export function EnhanceProWorkspace({
|
|||||||
|
|
||||||
setGeneratedImages(result.data.images);
|
setGeneratedImages(result.data.images);
|
||||||
setSelectedResultIndex(0);
|
setSelectedResultIndex(0);
|
||||||
|
requestIdRef.current = null;
|
||||||
|
requestSignatureRef.current = null;
|
||||||
toast.success(
|
toast.success(
|
||||||
`图像增强 Pro 完成,已生成 ${result.data.images.length} 张结果`
|
`图像增强 Pro 完成,已生成 ${result.data.images.length} 张结果`
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
error instanceof Error ? error.message : "图像增强 Pro 失败";
|
error instanceof Error ? error.message : "图像增强 Pro 失败";
|
||||||
|
if (!shouldPreserveEnhanceProRequestId(message)) {
|
||||||
|
requestIdRef.current = null;
|
||||||
|
requestSignatureRef.current = null;
|
||||||
|
}
|
||||||
setLastErrorMessage(message);
|
setLastErrorMessage(message);
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { Loader2, Sparkles } from "lucide-react";
|
import { Loader2, Sparkles } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@ -30,6 +30,44 @@ import { imageGenerationService, type EnhanceProImageResult } from "@/services/c
|
|||||||
|
|
||||||
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
function createEnhanceProClientRequestId() {
|
||||||
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}
|
||||||
|
return `enhance-pro-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldPreserveEnhanceProRequestId(message: string) {
|
||||||
|
return /任务.*已提交|避免重复提交|稍后重试查看结果|failed to fetch|network/i.test(
|
||||||
|
String(message || "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEnhanceProRequestSignature(params: {
|
||||||
|
imageUrl: string;
|
||||||
|
prompt: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
batchSize: number;
|
||||||
|
seed: number;
|
||||||
|
output_format: EnhanceProOutputFormat;
|
||||||
|
subject_detection: EnhanceProSubjectDetection;
|
||||||
|
face_enhancement: boolean;
|
||||||
|
face_enhancement_creativity: number;
|
||||||
|
face_enhancement_strength: number;
|
||||||
|
crop_to_fill: boolean;
|
||||||
|
model: EnhanceProKreaModel;
|
||||||
|
upscaling_activated: boolean;
|
||||||
|
image_scaling_factor: number;
|
||||||
|
creativity: number;
|
||||||
|
texture: number;
|
||||||
|
sharpen: number;
|
||||||
|
denoise: number;
|
||||||
|
detail: number;
|
||||||
|
}) {
|
||||||
|
return JSON.stringify(params);
|
||||||
|
}
|
||||||
|
|
||||||
type EnhanceProClientConfig = {
|
type EnhanceProClientConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
providerName: string;
|
providerName: string;
|
||||||
@ -196,6 +234,8 @@ export function EnhanceProDialog({
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [naturalSize, setNaturalSize] = useState({ width: 1024, height: 1024 });
|
const [naturalSize, setNaturalSize] = useState({ width: 1024, height: 1024 });
|
||||||
const [selectedPresetId, setSelectedPresetId] = useState("");
|
const [selectedPresetId, setSelectedPresetId] = useState("");
|
||||||
|
const requestIdRef = useRef<string | null>(null);
|
||||||
|
const requestSignatureRef = useRef<string | null>(null);
|
||||||
const [formState, setFormState] = useState<FormState>(() =>
|
const [formState, setFormState] = useState<FormState>(() =>
|
||||||
createDefaultFormState(FALLBACK_CONFIG, 1024, 1024),
|
createDefaultFormState(FALLBACK_CONFIG, 1024, 1024),
|
||||||
);
|
);
|
||||||
@ -262,34 +302,45 @@ export function EnhanceProDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
const requestParams = {
|
||||||
|
imageUrl,
|
||||||
|
prompt: formState.prompt.trim() || "图像增强 Pro",
|
||||||
|
width: clampNumber(Math.round(formState.width), 1, config.maxOutputEdge),
|
||||||
|
height: clampNumber(Math.round(formState.height), 1, config.maxOutputEdge),
|
||||||
|
batchSize: config.allowBatchSize
|
||||||
|
? clampNumber(Math.round(formState.batchSize), 1, config.maxBatchSize)
|
||||||
|
: 1,
|
||||||
|
seed: Math.round(formState.seed),
|
||||||
|
output_format: formState.output_format,
|
||||||
|
subject_detection: formState.subject_detection,
|
||||||
|
face_enhancement: formState.face_enhancement,
|
||||||
|
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,
|
||||||
|
upscaling_activated: formState.upscaling_activated,
|
||||||
|
image_scaling_factor: clampNumber(
|
||||||
|
formState.image_scaling_factor,
|
||||||
|
1,
|
||||||
|
config.maxUpscaleFactor
|
||||||
|
),
|
||||||
|
creativity: clampNumber(Math.round(formState.creativity), 1, 6),
|
||||||
|
texture: clampNumber(Math.round(formState.texture), 1, 5),
|
||||||
|
sharpen: clampNumber(formState.sharpen, 0, 1),
|
||||||
|
denoise: clampNumber(formState.denoise, 0, 1),
|
||||||
|
detail: clampNumber(formState.detail, 0, 1),
|
||||||
|
};
|
||||||
|
const requestSignature = buildEnhanceProRequestSignature(requestParams);
|
||||||
|
const clientRequestId =
|
||||||
|
requestIdRef.current && requestSignatureRef.current === requestSignature
|
||||||
|
? requestIdRef.current
|
||||||
|
: createEnhanceProClientRequestId();
|
||||||
|
requestIdRef.current = clientRequestId;
|
||||||
|
requestSignatureRef.current = requestSignature;
|
||||||
try {
|
try {
|
||||||
const result = await imageGenerationService.enhanceImagePro({
|
const result = await imageGenerationService.enhanceImagePro({
|
||||||
imageUrl,
|
...requestParams,
|
||||||
prompt: formState.prompt.trim() || "图像增强 Pro",
|
clientRequestId,
|
||||||
width: clampNumber(Math.round(formState.width), 1, config.maxOutputEdge),
|
|
||||||
height: clampNumber(Math.round(formState.height), 1, config.maxOutputEdge),
|
|
||||||
batchSize: config.allowBatchSize
|
|
||||||
? clampNumber(Math.round(formState.batchSize), 1, config.maxBatchSize)
|
|
||||||
: 1,
|
|
||||||
seed: Math.round(formState.seed),
|
|
||||||
output_format: formState.output_format,
|
|
||||||
subject_detection: formState.subject_detection,
|
|
||||||
face_enhancement: formState.face_enhancement,
|
|
||||||
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,
|
|
||||||
upscaling_activated: formState.upscaling_activated,
|
|
||||||
image_scaling_factor: clampNumber(
|
|
||||||
formState.image_scaling_factor,
|
|
||||||
1,
|
|
||||||
config.maxUpscaleFactor
|
|
||||||
),
|
|
||||||
creativity: clampNumber(Math.round(formState.creativity), 1, 6),
|
|
||||||
texture: clampNumber(Math.round(formState.texture), 1, 5),
|
|
||||||
sharpen: clampNumber(formState.sharpen, 0, 1),
|
|
||||||
denoise: clampNumber(formState.denoise, 0, 1),
|
|
||||||
detail: clampNumber(formState.detail, 0, 1),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.success || !result.data?.images?.length) {
|
if (!result.success || !result.data?.images?.length) {
|
||||||
@ -297,10 +348,17 @@ export function EnhanceProDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
onComplete?.(result.data.images);
|
onComplete?.(result.data.images);
|
||||||
|
requestIdRef.current = null;
|
||||||
|
requestSignatureRef.current = null;
|
||||||
toast.success(`图像增强 Pro 完成,已生成 ${result.data.images.length} 张结果`);
|
toast.success(`图像增强 Pro 完成,已生成 ${result.data.images.length} 张结果`);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "图像增强 Pro 失败");
|
const message = error instanceof Error ? error.message : "图像增强 Pro 失败";
|
||||||
|
if (!shouldPreserveEnhanceProRequestId(message)) {
|
||||||
|
requestIdRef.current = null;
|
||||||
|
requestSignatureRef.current = null;
|
||||||
|
}
|
||||||
|
toast.error(message);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,14 @@ function buildTaskError(payload: LongTaskResponse | null | undefined, fallback:
|
|||||||
return new Error(String(message || fallback));
|
return new Error(String(message || fallback));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildTaskInterruptedError(taskId: string) {
|
||||||
|
const error = new Error(
|
||||||
|
"任务已提交,网络连接暂时中断,请稍后重试查看结果,避免重复提交。"
|
||||||
|
) as Error & { taskId?: string };
|
||||||
|
error.taskId = taskId;
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
async function safeParseJson(response: Response) {
|
async function safeParseJson(response: Response) {
|
||||||
try {
|
try {
|
||||||
return (await response.json()) as LongTaskResponse;
|
return (await response.json()) as LongTaskResponse;
|
||||||
@ -132,9 +140,7 @@ export async function requestLongTaskResult(params: {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
consecutivePollFailures += 1;
|
consecutivePollFailures += 1;
|
||||||
if (consecutivePollFailures >= maxPollFailures) {
|
if (consecutivePollFailures >= maxPollFailures) {
|
||||||
throw error instanceof Error
|
throw buildTaskInterruptedError(taskId);
|
||||||
? error
|
|
||||||
: new Error("查询任务状态失败");
|
|
||||||
}
|
}
|
||||||
pollAfterMs = resolveRetryAfterMs(null, null, pollAfterMs, consecutivePollFailures);
|
pollAfterMs = resolveRetryAfterMs(null, null, pollAfterMs, consecutivePollFailures);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
27
lib/db/migrations/0074_add_enhance_pro_tasks.sql
Normal file
27
lib/db/migrations/0074_add_enhance_pro_tasks.sql
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "EnhanceProTask" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"userId" uuid NOT NULL REFERENCES "User"("id") ON DELETE cascade,
|
||||||
|
"chatId" uuid REFERENCES "Chat"("id") ON DELETE cascade,
|
||||||
|
"messageId" uuid,
|
||||||
|
"clientRequestId" varchar(64),
|
||||||
|
"providerTaskId" varchar(128) NOT NULL,
|
||||||
|
"taskKeyId" varchar(64),
|
||||||
|
"status" varchar(16) NOT NULL DEFAULT 'pending',
|
||||||
|
"prompt" text,
|
||||||
|
"sourceImageUrl" text NOT NULL,
|
||||||
|
"request" jsonb NOT NULL,
|
||||||
|
"result" jsonb,
|
||||||
|
"error" text,
|
||||||
|
"pointsCost" integer NOT NULL DEFAULT 0,
|
||||||
|
"pointsCharged" boolean NOT NULL DEFAULT false,
|
||||||
|
"chargedAt" timestamp,
|
||||||
|
"completedAt" timestamp,
|
||||||
|
"createdAt" timestamp NOT NULL DEFAULT now(),
|
||||||
|
"updatedAt" timestamp NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "EnhanceProTask_providerTaskId_idx"
|
||||||
|
ON "EnhanceProTask" ("providerTaskId");
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "EnhanceProTask_userId_clientRequestId_idx"
|
||||||
|
ON "EnhanceProTask" ("userId", "clientRequestId");
|
||||||
@ -421,6 +421,13 @@
|
|||||||
"when": 1776336730695,
|
"when": 1776336730695,
|
||||||
"tag": "0073_add_system_setting_history",
|
"tag": "0073_add_system_setting_history",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 60,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1777252800000,
|
||||||
|
"tag": "0074_add_enhance_pro_tasks",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -751,6 +751,34 @@ export const canvasImageTask = pgTable("CanvasImageTask", {
|
|||||||
|
|
||||||
export type CanvasImageTask = InferSelectModel<typeof canvasImageTask>;
|
export type CanvasImageTask = InferSelectModel<typeof canvasImageTask>;
|
||||||
|
|
||||||
|
export const enhanceProTask = pgTable("EnhanceProTask", {
|
||||||
|
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||||
|
userId: uuid("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
chatId: uuid("chatId").references(() => chat.id, { onDelete: "cascade" }),
|
||||||
|
messageId: uuid("messageId"),
|
||||||
|
clientRequestId: varchar("clientRequestId", { length: 64 }),
|
||||||
|
providerTaskId: varchar("providerTaskId", { length: 128 }).notNull(),
|
||||||
|
taskKeyId: varchar("taskKeyId", { length: 64 }),
|
||||||
|
status: varchar("status", { length: 16 })
|
||||||
|
.notNull()
|
||||||
|
.default("pending"), // pending | running | finalizing | succeeded | failed
|
||||||
|
prompt: text("prompt"),
|
||||||
|
sourceImageUrl: text("sourceImageUrl").notNull(),
|
||||||
|
request: jsonb("request").notNull(),
|
||||||
|
result: jsonb("result"),
|
||||||
|
error: text("error"),
|
||||||
|
pointsCost: integer("pointsCost").notNull().default(0),
|
||||||
|
pointsCharged: boolean("pointsCharged").notNull().default(false),
|
||||||
|
chargedAt: timestamp("chargedAt"),
|
||||||
|
completedAt: timestamp("completedAt"),
|
||||||
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type EnhanceProTask = InferSelectModel<typeof enhanceProTask>;
|
||||||
|
|
||||||
export const agentPsdRenderTask = pgTable("AgentPsdRenderTask", {
|
export const agentPsdRenderTask = pgTable("AgentPsdRenderTask", {
|
||||||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||||
userId: uuid("userId")
|
userId: uuid("userId")
|
||||||
|
|||||||
@ -65,6 +65,7 @@ export interface EnhanceProRequestParams {
|
|||||||
prompt?: string;
|
prompt?: string;
|
||||||
chatId?: string | null;
|
chatId?: string | null;
|
||||||
messageId?: string | null;
|
messageId?: string | null;
|
||||||
|
clientRequestId?: string | null;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
batchSize?: number;
|
batchSize?: number;
|
||||||
@ -388,14 +389,6 @@ export const imageGenerationService = {
|
|||||||
const payload = await requestLongTaskResult({
|
const payload = await requestLongTaskResult({
|
||||||
endpoint: '/api/enhance/pro',
|
endpoint: '/api/enhance/pro',
|
||||||
payload: params,
|
payload: params,
|
||||||
pollingParams: {
|
|
||||||
prompt: params.prompt || '图像增强 Pro',
|
|
||||||
chatId: params.chatId,
|
|
||||||
messageId: params.messageId,
|
|
||||||
sourceImageUrl: params.imageUrl,
|
|
||||||
width: params.width,
|
|
||||||
height: params.height,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!payload?.success) {
|
if (!payload?.success) {
|
||||||
@ -440,6 +433,12 @@ export const imageGenerationService = {
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Canvas] 图像增强 Pro 失败:', error);
|
console.error('[Canvas] 图像增强 Pro 失败:', error);
|
||||||
|
if (error instanceof Error && /failed to fetch|network/i.test(error.message)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '任务可能已提交,网络连接暂时中断,请稍后重试查看结果,避免重复提交。',
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : '图像增强 Pro 失败',
|
error: error instanceof Error ? error.message : '图像增强 Pro 失败',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user