import { type NextRequest, NextResponse } from "next/server"; import { auth } from "@/app/(auth)/auth"; import { ExtendServiceError, executeExtendJob } from "@/lib/ai/extend/service"; import { getPointsSystemConfig } from "@/lib/db/queries"; import { prepareFeaturePointsCharge } from "@/lib/points/feature-charge"; import { getImageToolCreditCost } from "@/lib/points/config"; import { getFriendlyRateLimitMessage, getUserFacingRequestErrorMessage, isRateLimitError, } from "@/lib/request-error"; export const maxDuration = 180; function parseIncludeBase64(value: unknown): boolean { if (typeof value === "boolean") return value; const normalized = String(value ?? "").trim().toLowerCase(); return normalized === "true" || normalized === "1" || normalized === "yes"; } function buildExtendSuccessPayload(result: Awaited>) { return { success: true, original_url: result.original_url, thumbnail_url: result.thumbnail_url, ...(result.image_base64 ? { base64: result.image_base64 } : {}), processing_time: result.processing_time, }; } export async function POST(request: NextRequest) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ error: "未授权,请先登录" }, { status: 401 }); } const contentType = request.headers.get("content-type") || ""; let imageUrl: string | null = null; let imageBase64: string | null = null; let prompt = "图像拓展处理"; let chatId: string | null = null; let messageId: string | null = null; let includeBase64 = false; const requestId = `extend-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; if (contentType.includes("application/json")) { const body = await request.json(); imageUrl = body.image_url ? String(body.image_url).trim() : null; imageBase64 = body.image_base64 ? String(body.image_base64).trim() : null; prompt = body.prompt || prompt; chatId = body.chatId || null; messageId = body.messageId || null; includeBase64 = parseIncludeBase64(body.includeBase64); if (!imageUrl && !imageBase64) { return NextResponse.json( { error: "缺少image_base64或image_url参数" }, { status: 400 }, ); } console.log("[Extend API] Request received:", { requestId, source: imageUrl ? "url" : "base64", includeBase64, chatId, messageId, xScale: body.xScale, yScale: body.yScale, }); const pointsConfig = await getPointsSystemConfig(); const pointsCharge = await prepareFeaturePointsCharge({ userId: session.user.id, unitCost: getImageToolCreditCost(pointsConfig, "extend"), reason: "image_extend", modelId: "image-extend", note: "图片扩图消耗积分", pointsConfig, }); if (!pointsCharge.ok) { return NextResponse.json(pointsCharge.payload, { status: pointsCharge.status }); } const result = await executeExtendJob({ userId: session.user.id, imageUrl, imageBase64, prompt, chatId, messageId, xScale: body.xScale ? Number(body.xScale) : undefined, yScale: body.yScale ? Number(body.yScale) : undefined, includeBase64, requestId, }); if (pointsCharge.chargeable) { try { await pointsCharge.consume(); } catch (pointsError) { console.error("[Points] Failed to consume extend points:", pointsError); } } console.log("[Extend API] Request completed:", { requestId, includeBase64, processingTime: result.processing_time, originalUrl: result.original_url.slice(0, 120), }); return NextResponse.json(buildExtendSuccessPayload(result)); } if (contentType.includes("multipart/form-data")) { const formData = await request.formData(); const file = formData.get("file") as File | null; if (!file) { return NextResponse.json({ error: "缺少file参数" }, { status: 400 }); } const arrayBuffer = await file.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); const multipartChatId = String(formData.get("chatId") || "") || null; const multipartMessageId = String(formData.get("messageId") || "") || null; includeBase64 = parseIncludeBase64(formData.get("includeBase64")); console.log("[Extend API] Multipart request received:", { requestId, includeBase64, chatId: multipartChatId, messageId: multipartMessageId, bytes: buffer.length, }); const pointsConfig = await getPointsSystemConfig(); const pointsCharge = await prepareFeaturePointsCharge({ userId: session.user.id, unitCost: getImageToolCreditCost(pointsConfig, "extend"), reason: "image_extend", modelId: "image-extend", note: "图片扩图消耗积分", pointsConfig, }); if (!pointsCharge.ok) { return NextResponse.json(pointsCharge.payload, { status: pointsCharge.status }); } const result = await executeExtendJob({ userId: session.user.id, imageBase64: buffer.toString("base64"), prompt: String(formData.get("prompt") || prompt), chatId: multipartChatId, messageId: multipartMessageId, includeBase64, requestId, }); if (pointsCharge.chargeable) { try { await pointsCharge.consume(); } catch (pointsError) { console.error("[Points] Failed to consume extend points:", pointsError); } } console.log("[Extend API] Multipart request completed:", { requestId, includeBase64, processingTime: result.processing_time, originalUrl: result.original_url.slice(0, 120), }); return NextResponse.json(buildExtendSuccessPayload(result)); } return NextResponse.json( { error: "不支持的Content-Type,请使用application/json或multipart/form-data" }, { status: 400 }, ); } catch (error) { if (error instanceof ExtendServiceError) { if (error.status === 429 || isRateLimitError(error)) { return NextResponse.json( { error: getFriendlyRateLimitMessage("当前操作过于频繁,请稍后再试"), detail: error.detail || error.message, retryAfter: 10, }, { status: 429, headers: { "Retry-After": "10", }, }, ); } return NextResponse.json( { error: getUserFacingRequestErrorMessage(error, "图像拓展失败"), detail: error.detail || error.message, }, { status: error.status }, ); } console.error("[Extend API] 错误:", error); if (isRateLimitError(error)) { return NextResponse.json( { error: getFriendlyRateLimitMessage("当前操作过于频繁,请稍后再试"), detail: error instanceof Error ? error.message : String(error), retryAfter: 10, }, { status: 429, headers: { "Retry-After": "10", }, }, ); } return NextResponse.json( { error: getUserFacingRequestErrorMessage(error, "图像拓展失败"), detail: error instanceof Error ? error.message : String(error), }, { status: 500 }, ); } }