Files
pagodera/app/api/admin/items/analyze/route.ts
T
Evandro Fonseca Junior d3704602fd feat(items): add server field and improve item management
refactor(ai): enhance item structure generation with schema validation
feat(ai): add token usage tracking and cost calculation
fix(ui): improve item card number formatting with locale support
docs(items): add documentation for item value separation
test(items): add tests for value field validation
2025-11-11 03:04:49 -03:00

111 lines
3.5 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { requireAdminUser } from "@/lib/auth/middleware";
import { generateItemStructure } from "@/lib/ai/generate-item-structure";
import { calculateTokenCost } from "@/lib/ai/token-pricing";
import { recordTokenUsage } from "@/lib/ai/token-usage";
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 }
);
}