111 lines
3.5 KiB
TypeScript
111 lines
3.5 KiB
TypeScript
import { type NextRequest, NextResponse } from "next/server";
|
|
import { generateItemStructure } from "@/lib/ai/generate-item-structure";
|
|
import { calculateTokenCost } from "@/lib/ai/token-pricing";
|
|
import { recordTokenUsage } from "@/lib/ai/token-usage";
|
|
import { requireAdminUser } from "@/lib/auth/middleware";
|
|
|
|
export async function POST(request: NextRequest) {
|
|
const auth = await requireAdminUser(request);
|
|
if (auth instanceof NextResponse) {
|
|
return auth;
|
|
}
|
|
|
|
const body = await request.json();
|
|
const name = typeof body?.name === "string" ? body.name.trim() : "";
|
|
const description =
|
|
typeof body?.description === "string" ? body.description.trim() : "";
|
|
const scope = body?.scope === "server" ? "server" : "global";
|
|
const server =
|
|
scope === "server" && typeof body?.server === "string"
|
|
? body.server.trim()
|
|
: null;
|
|
const categories: string[] = Array.isArray(body?.categories)
|
|
? body.categories.filter((item: unknown) => typeof item === "string")
|
|
: [];
|
|
const servers: string[] = Array.isArray(body?.servers)
|
|
? body.servers.filter((item: unknown) => typeof item === "string")
|
|
: [];
|
|
const images: string[] = Array.isArray(body?.images)
|
|
? body.images.filter((item: unknown) => typeof item === "string")
|
|
: [];
|
|
const hasDescription = !!description;
|
|
const hasImages = images.length > 0;
|
|
|
|
// Nova regra: aceitar quando houver descrição OU ao menos uma imagem.
|
|
// Nome pode ser vazio; o assistente do front lida com sugestão/confirmar_nome.
|
|
if (!hasDescription && !hasImages) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Forneça descrição ou ao menos uma imagem para análise.",
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const result = await generateItemStructure({
|
|
name,
|
|
description,
|
|
scope,
|
|
server,
|
|
categories,
|
|
servers,
|
|
images: images
|
|
.slice(0, 2)
|
|
.map((dataUrl) => {
|
|
const [meta, base64Part] = dataUrl.split(",");
|
|
const mimeMatch = meta?.match(/data:(.*);base64/);
|
|
return {
|
|
mimeType: mimeMatch?.[1] ?? "image/jpeg",
|
|
base64Data: base64Part ?? meta,
|
|
};
|
|
})
|
|
.filter((image) => image.base64Data),
|
|
});
|
|
|
|
// Cálculo de custo baseado em usageMetadata
|
|
const _model = (result.model ??
|
|
"gemini-2.5-flash") as keyof typeof import("@/lib/ai/token-pricing").TOKEN_PRICING;
|
|
const breakdown = calculateTokenCost("gemini-2.5-flash", result.usage);
|
|
|
|
// Persistir histórico para relatórios (não bloqueia resposta; erros são logados no Redis util)
|
|
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
try {
|
|
await recordTokenUsage({
|
|
id: requestId,
|
|
timestamp: Date.now(),
|
|
model: result.model ?? "gemini-2.5-flash",
|
|
inputTokens: breakdown.inputTokens,
|
|
outputTokens: breakdown.outputTokens,
|
|
totalTokens: breakdown.totalTokens,
|
|
costUsd: breakdown.totalUsd,
|
|
context: {
|
|
route: "/api/admin/items/analyze",
|
|
scope,
|
|
server,
|
|
hasImages,
|
|
hasDescription,
|
|
categoriesCount: categories.length,
|
|
},
|
|
});
|
|
} catch {
|
|
// Falhas de Redis não devem impedir a análise
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
structure: result.structure,
|
|
usage: {
|
|
promptTokens: breakdown.inputTokens,
|
|
outputTokens: breakdown.outputTokens,
|
|
totalTokens: breakdown.totalTokens,
|
|
},
|
|
costUsd: breakdown.totalUsd,
|
|
model: result.model ?? "gemini-2.5-flash",
|
|
requestId,
|
|
},
|
|
{ status: 200 },
|
|
);
|
|
}
|