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 { and, eq, or } from "drizzle-orm";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { db } from "@/lib/db";
|
||||
@ -11,7 +12,11 @@ import {
|
||||
getUserById,
|
||||
upsertMediaAsset,
|
||||
} from "@/lib/db/queries";
|
||||
import { generatedImage } from "@/lib/db/schema";
|
||||
import {
|
||||
enhanceProTask,
|
||||
generatedImage,
|
||||
pointsLedger,
|
||||
} from "@/lib/db/schema";
|
||||
import {
|
||||
ENHANCE_PRO_KREA_SETTING_KEY,
|
||||
normalizeEnhanceProKreaConfig,
|
||||
@ -24,7 +29,6 @@ import { calculateEnhanceProPointsEstimate } from "@/lib/points/config";
|
||||
import { uploadFile } from "@/lib/storage/service";
|
||||
|
||||
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_MODEL_ID = "krea-topaz-generative";
|
||||
const ENHANCE_PRO_MAX_RESULT_IMAGES = 4;
|
||||
@ -37,6 +41,7 @@ type EnhanceProRequestBody = {
|
||||
prompt?: string;
|
||||
chatId?: string | null;
|
||||
messageId?: string | null;
|
||||
clientRequestId?: string | null;
|
||||
width?: number;
|
||||
height?: number;
|
||||
batchSize?: number;
|
||||
@ -64,6 +69,27 @@ type FinalizedImageResult = {
|
||||
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) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return fallback;
|
||||
@ -85,6 +111,10 @@ function normalizeJobStatus(value: unknown) {
|
||||
return String(value || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function normalizeClientRequestId(value: unknown) {
|
||||
return String(value || "").trim().slice(0, 64);
|
||||
}
|
||||
|
||||
function getEnhanceProKeyFingerprint(apiKey: string) {
|
||||
return createHash("sha256")
|
||||
.update(String(apiKey || "").trim())
|
||||
@ -126,14 +156,13 @@ function resolveEnhanceProApiKey(
|
||||
};
|
||||
}
|
||||
|
||||
function buildEnhanceProTaskPendingResponse(taskId: string, taskKeyId?: string | null) {
|
||||
function buildEnhanceProTaskPendingResponse(taskId: string) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
processing: true,
|
||||
status: "running",
|
||||
taskId,
|
||||
taskKeyId,
|
||||
pollAfterMs: ENHANCE_PRO_TASK_POLL_INTERVAL_MS,
|
||||
},
|
||||
{ status: 202 }
|
||||
@ -252,7 +281,8 @@ async function resolveSourceInput(body: EnhanceProRequestBody) {
|
||||
}
|
||||
|
||||
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) {
|
||||
return {
|
||||
sourceImageUrl: imageUrl,
|
||||
@ -351,12 +381,7 @@ async function createEnhanceProJob(params: {
|
||||
}
|
||||
}
|
||||
|
||||
async function pollEnhanceProJob(
|
||||
config: EnhanceProKreaConfig,
|
||||
jobId: string,
|
||||
delay = ENHANCE_PRO_TASK_POLL_INTERVAL_MS,
|
||||
timeout = ENHANCE_PRO_SYNC_WAIT_TIMEOUT_MS,
|
||||
): Promise<any> {
|
||||
async function fetchEnhanceProJobStatus(config: EnhanceProKreaConfig, jobId: string) {
|
||||
const apiUrl = normalizeBaseUrl(config.baseUrl);
|
||||
const response = await fetch(`${apiUrl}/jobs/${jobId}`, {
|
||||
method: "GET",
|
||||
@ -371,27 +396,7 @@ async function pollEnhanceProJob(
|
||||
throw new Error(`查询任务状态失败: ${response.status} ${detail || response.statusText}`);
|
||||
}
|
||||
|
||||
const payload = 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 || "");
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
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() {
|
||||
const setting = await getSystemSetting({ key: ENHANCE_PRO_KREA_SETTING_KEY });
|
||||
const existingValue =
|
||||
@ -558,50 +792,76 @@ export async function GET(request: NextRequest) {
|
||||
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();
|
||||
if (!config.enabled) {
|
||||
return NextResponse.json({ error: "图像增强 Pro 功能未开启" }, { status: 403 });
|
||||
}
|
||||
const keySelection = resolveEnhanceProApiKey(config, searchParams.get("taskKeyId"));
|
||||
const keySelection = resolveEnhanceProApiKey(config, task.taskKeyId);
|
||||
if (!keySelection || !config.baseUrl) {
|
||||
return NextResponse.json({ error: "图像增强 Pro API 尚未配置" }, { status: 400 });
|
||||
}
|
||||
|
||||
let taskResult: any;
|
||||
try {
|
||||
taskResult = await pollEnhanceProJob(
|
||||
{ ...config, apiKey: keySelection.apiKey },
|
||||
taskId
|
||||
);
|
||||
} catch (pollError) {
|
||||
if (isEnhanceProTaskStillProcessing(pollError)) {
|
||||
return buildEnhanceProTaskPendingResponse(taskId, keySelection.keyId);
|
||||
const taskResult = await fetchEnhanceProJobStatus(
|
||||
{ ...config, apiKey: keySelection.apiKey },
|
||||
task.providerTaskId,
|
||||
);
|
||||
const providerStatus = normalizeJobStatus(taskResult?.status);
|
||||
|
||||
if (providerStatus === "completed") {
|
||||
const completedPayload = await finalizeEnhanceProTask(task, taskResult);
|
||||
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(
|
||||
{
|
||||
error: "图像增强 Pro 处理失败",
|
||||
detail: pollError instanceof Error ? pollError.message : "轮询失败",
|
||||
detail,
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
const width = sanitizeDimension(searchParams.get("width"), 1024);
|
||||
const height = sanitizeDimension(searchParams.get("height"), 1024);
|
||||
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);
|
||||
await markEnhanceProTaskRunning(task.id);
|
||||
return buildEnhanceProTaskPendingResponse(task.id);
|
||||
} catch (error) {
|
||||
console.error("[Enhance Pro API][TaskStatus] 错误:", error);
|
||||
return NextResponse.json(
|
||||
@ -629,6 +889,24 @@ export async function POST(request: NextRequest) {
|
||||
const requestConfig = { ...config, apiKey: keySelection.apiKey };
|
||||
|
||||
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 chatId = body.chatId || null;
|
||||
const messageId = body.messageId || null;
|
||||
@ -706,78 +984,100 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
|
||||
const createdStatus = normalizeJobStatus(createdJob?.status);
|
||||
const taskId = String(createdJob?.job_id || "").trim();
|
||||
if (!taskId) {
|
||||
const providerTaskId = String(createdJob?.job_id || "").trim();
|
||||
if (!providerTaskId) {
|
||||
return NextResponse.json(
|
||||
{ error: "创建图像增强 Pro 任务失败", detail: "Krea 未返回 job_id" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
if (pointsEstimate.chargeable) {
|
||||
try {
|
||||
await consumeUserPoints({
|
||||
const storedRequest: EnhanceProStoredRequest = {
|
||||
prompt: prompt || "图像增强 Pro",
|
||||
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,
|
||||
amount: pointsEstimate.cost,
|
||||
reason: "enhance_pro_generation",
|
||||
modelId: `${ENHANCE_PRO_MODEL_ID}:${selectedModel}`,
|
||||
chatId,
|
||||
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) {
|
||||
console.error("[Points] Failed to consume Enhance Pro points:", pointsError);
|
||||
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);
|
||||
}
|
||||
}
|
||||
throw insertError;
|
||||
}
|
||||
|
||||
if (createdStatus === "completed") {
|
||||
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);
|
||||
}
|
||||
|
||||
if (!createdTask) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "图像增强 Pro 处理失败",
|
||||
detail: pollError instanceof Error ? pollError.message : "轮询失败",
|
||||
},
|
||||
{ error: "创建图像增强 Pro 任务失败", detail: "任务入库失败" },
|
||||
{ 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) {
|
||||
console.error("[Enhance Pro API] 错误:", error);
|
||||
return NextResponse.json(
|
||||
|
||||
@ -60,6 +60,46 @@ import {
|
||||
|
||||
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 = {
|
||||
enabled: boolean;
|
||||
providerName: string;
|
||||
@ -398,6 +438,8 @@ export function EnhanceProWorkspace({
|
||||
const [faceOpen, setFaceOpen] = useState(true);
|
||||
const [comparePosition, setComparePosition] = useState(50);
|
||||
const compareDragPointerIdRef = useRef<number | null>(null);
|
||||
const requestIdRef = useRef<string | null>(null);
|
||||
const requestSignatureRef = useRef<string | null>(null);
|
||||
|
||||
const { data: configPayload, isLoading } = useSWR<{
|
||||
success?: boolean;
|
||||
@ -552,50 +594,61 @@ export function EnhanceProWorkspace({
|
||||
|
||||
setIsSubmitting(true);
|
||||
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 {
|
||||
const result = await imageGenerationService.enhanceImagePro({
|
||||
...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),
|
||||
...requestParams,
|
||||
clientRequestId,
|
||||
});
|
||||
|
||||
if (!result.success || !result.data?.images?.length) {
|
||||
@ -604,12 +657,18 @@ export function EnhanceProWorkspace({
|
||||
|
||||
setGeneratedImages(result.data.images);
|
||||
setSelectedResultIndex(0);
|
||||
requestIdRef.current = null;
|
||||
requestSignatureRef.current = null;
|
||||
toast.success(
|
||||
`图像增强 Pro 完成,已生成 ${result.data.images.length} 张结果`
|
||||
);
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "图像增强 Pro 失败";
|
||||
if (!shouldPreserveEnhanceProRequestId(message)) {
|
||||
requestIdRef.current = null;
|
||||
requestSignatureRef.current = null;
|
||||
}
|
||||
setLastErrorMessage(message);
|
||||
toast.error(message);
|
||||
} finally {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import { Loader2, Sparkles } from "lucide-react";
|
||||
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());
|
||||
|
||||
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 = {
|
||||
enabled: boolean;
|
||||
providerName: string;
|
||||
@ -196,6 +234,8 @@ export function EnhanceProDialog({
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [naturalSize, setNaturalSize] = useState({ width: 1024, height: 1024 });
|
||||
const [selectedPresetId, setSelectedPresetId] = useState("");
|
||||
const requestIdRef = useRef<string | null>(null);
|
||||
const requestSignatureRef = useRef<string | null>(null);
|
||||
const [formState, setFormState] = useState<FormState>(() =>
|
||||
createDefaultFormState(FALLBACK_CONFIG, 1024, 1024),
|
||||
);
|
||||
@ -262,34 +302,45 @@ export function EnhanceProDialog({
|
||||
}
|
||||
|
||||
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 {
|
||||
const result = await imageGenerationService.enhanceImagePro({
|
||||
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),
|
||||
...requestParams,
|
||||
clientRequestId,
|
||||
});
|
||||
|
||||
if (!result.success || !result.data?.images?.length) {
|
||||
@ -297,10 +348,17 @@ export function EnhanceProDialog({
|
||||
}
|
||||
|
||||
onComplete?.(result.data.images);
|
||||
requestIdRef.current = null;
|
||||
requestSignatureRef.current = null;
|
||||
toast.success(`图像增强 Pro 完成,已生成 ${result.data.images.length} 张结果`);
|
||||
onOpenChange(false);
|
||||
} 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 {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
||||
@ -30,6 +30,14 @@ function buildTaskError(payload: LongTaskResponse | null | undefined, 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) {
|
||||
try {
|
||||
return (await response.json()) as LongTaskResponse;
|
||||
@ -132,9 +140,7 @@ export async function requestLongTaskResult(params: {
|
||||
} catch (error) {
|
||||
consecutivePollFailures += 1;
|
||||
if (consecutivePollFailures >= maxPollFailures) {
|
||||
throw error instanceof Error
|
||||
? error
|
||||
: new Error("查询任务状态失败");
|
||||
throw buildTaskInterruptedError(taskId);
|
||||
}
|
||||
pollAfterMs = resolveRetryAfterMs(null, null, pollAfterMs, consecutivePollFailures);
|
||||
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,
|
||||
"tag": "0073_add_system_setting_history",
|
||||
"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 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", {
|
||||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||
userId: uuid("userId")
|
||||
|
||||
@ -65,6 +65,7 @@ export interface EnhanceProRequestParams {
|
||||
prompt?: string;
|
||||
chatId?: string | null;
|
||||
messageId?: string | null;
|
||||
clientRequestId?: string | null;
|
||||
width: number;
|
||||
height: number;
|
||||
batchSize?: number;
|
||||
@ -388,14 +389,6 @@ export const imageGenerationService = {
|
||||
const payload = await requestLongTaskResult({
|
||||
endpoint: '/api/enhance/pro',
|
||||
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) {
|
||||
@ -440,6 +433,12 @@ export const imageGenerationService = {
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[Canvas] 图像增强 Pro 失败:', error);
|
||||
if (error instanceof Error && /failed to fetch|network/i.test(error.message)) {
|
||||
return {
|
||||
success: false,
|
||||
error: '任务可能已提交,网络连接暂时中断,请稍后重试查看结果,避免重复提交。',
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '图像增强 Pro 失败',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user