import { buildGeminiParts, buildItemPrompt, type InlineImageData, } from "@/lib/ai/item-prompt"; import { listCategoryTemplates } from "@/lib/items/sqlite-manager"; import { secureLogger } from "@/lib/secure-logger"; interface GenerateItemStructureInput { name: string; description: string; scope: "global" | "server"; server?: string | null; categories: string[]; servers: string[]; images?: InlineImageData[]; } interface GeminiResponse { structure: { table: string; columns: Record; }; raw: unknown; usage?: { promptTokenCount?: number; candidatesTokenCount?: number; totalTokenCount?: number; }; model?: string; } const FALLBACK_STRUCTURE = { table: "items_all", columns: { name: "Final Fate", description: "Usado no slot de tocha. Bônus em leech, velocidade e proteção física.", attributes: { shielding: 20, death_magic: 120, mana_leech: { value: 30, chance: 100 }, life_leech: { value: 25, chance: 100 }, critical_hit: { damage: 25, chance: 75 }, speed: 1000, protection_physical: 40, damage_reflection: 80, }, }, }; export async function generateItemStructure( input: GenerateItemStructureInput, ): Promise { const apiKey = process.env.GEMINI_API_KEY; if (!apiKey) { secureLogger.warn( "⚠️ GEMINI_API_KEY ausente – utilizando fallback estático.", ); return { structure: FALLBACK_STRUCTURE, raw: FALLBACK_STRUCTURE, }; } const url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent"; // Coleta estrutura do SQLite (somente nomes de tabelas, colunas e tipos) e injeta no prompt. let sqliteSchemaText = "- Nenhuma tabela encontrada"; try { const templates = listCategoryTemplates(); if (templates.length > 0) { const lines: string[] = ["Estruturas atuais no SQLite:"]; templates.forEach((tpl) => { lines.push(`- Tabela: ${tpl.name}`); tpl.columns.forEach((col) => { lines.push(` - ${col.name}: ${col.type}`); }); }); sqliteSchemaText = lines.join("\n"); } } catch (_e) { sqliteSchemaText = "[Falha ao listar SQLite]"; } const extra = [ // Instruções específicas para exigência de saída fixa "Inclua SEMPRE o campo fixo 'schemaSuggestion' com a melhor tabela existente ou a sugestão de nova, e a lista de colunas com tipos (TEXT|INTEGER|REAL|BOOLEAN).", "Nunca inclua dados reais do SQLite; use somente a estrutura listada.", // Reforços de consistência e proibições "Para escopo global, a tabela deve ser 'items_all'. Para escopo de servidor, use 'items_' com slug minúsculo, '_' como separador, sem caracteres não alfanuméricos.", "Servidores em 'columns.servers' DEVEM pertencer à lista fornecida no contexto; prefira um array de strings.", "Em 'columns.metadata', insira apenas atributos descritivos. É proibido incluir campos de valor/preço/mercado: price, value, buy, sell, currency, npc_price, marketPrice, minPrice, maxPrice, avgPrice.", "Garanta que 'columns.description' tenha no máximo 100 caracteres; se maior, recorte exatamente os primeiros 100 sem sufixos.", sqliteSchemaText, ].join("\n"); const prompt = await buildItemPrompt( { name: input.name, description: input.description, scope: input.scope, server: input.server, categories: input.categories, servers: input.servers, }, [ input.images && input.images.length > 0 ? "Duas imagens de contexto foram fornecidas como partes inline (após o prompt). Utilize-as para compreender atributos visuais." : null, extra, ] .filter(Boolean) .join("\n"), ); const parts = await buildGeminiParts(prompt, input.images); async function callOnce(): Promise { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", ...(apiKey ? { "x-goog-api-key": apiKey } : {}), }, body: JSON.stringify({ contents: [ { role: "user", parts, }, ], generationConfig: { temperature: 0, topP: 0.1, }, }), }); const bodyText = await response.text(); const bodyJson = (() => { try { return JSON.parse(bodyText); } catch { return { raw: bodyText }; } })(); if (!response.ok) { const status = response.status; const msg = bodyJson?.error?.message || `Gemini retornou status ${status}`; const err = new Error(msg); (err as any).status = status; (err as any).response = bodyJson; throw err; } return bodyJson; } try { // Retry simples para 429 (rate limit): até 2 tentativas com backoff. let attempts = 0; let data: any; while (true) { try { data = await callOnce(); break; } catch (e) { const status = (e as any)?.status; attempts++; if (status === 429 && attempts <= 2) { const delayMs = 500 * attempts; await new Promise((r) => setTimeout(r, delayMs)); continue; } throw e; } } const text = data?.candidates?.[0]?.content?.parts?.[0]?.text ?? data?.candidates?.[0]?.output_text; if (!text) { throw new Error("Resposta do Gemini vazia ou inválida."); } const cleaned = text .replace(/```json/gi, "") .replace(/```/g, "") .trim(); const parsed = JSON.parse(cleaned); return { structure: parsed, raw: data, usage: data?.usageMetadata, model: "gemini-2.5-flash", }; } catch (error) { secureLogger.error("❌ Falha ao gerar estrutura via Gemini", { error: error instanceof Error ? error.message : "Erro desconhecido", status: (error as any)?.status, response: (error as any)?.response, }); return { structure: FALLBACK_STRUCTURE, raw: FALLBACK_STRUCTURE, usage: undefined, model: "gemini-2.5-flash", }; } } function _buildPrompt({ name, description, scope, server, }: GenerateItemStructureInput) { return ` Você é um assistente que prepara carga útil em JSON para inserção de itens em banco de dados. Requisitos obrigatórios: - Responda SOMENTE com JSON válido, sem comentários, sem markdown, sem texto adicional. - Formato esperado: { "table": "nome_da_tabela", "columns": { "...": "..." } } - Nunca inclua explicações fora do JSON. Contexto do item: - Nome: ${name} - Escopo: ${scope === "server" ? `específico do servidor ${server ?? "não informado"}` : "global"} - Descrição original (mantenha estrutura e extrair atributos quando possível): """ ${description} """ Instruções: - Escolha uma tabela coerente com o escopo (ex.: "items_all" para global ou "items_${( server ?? "custom" ) .toLowerCase() .replace(/[^a-z0-9]+/g, "_")}" para servidor). - Dentro de "columns", inicie com "name" e "description". - Modele atributos numéricos em objetos ou subtabelas conforme necessário (ex.: {"attributes": {...}}). - Quando não tiver certeza de um campo, deixe nulo. - Nunca invente dados além do que estiver na descrição. - Garante que o JSON seja minuciosamente válido. `; }