178 lines
5.8 KiB
TypeScript
178 lines
5.8 KiB
TypeScript
import { promises as fs } from "node:fs";
|
|
import path from "node:path";
|
|
|
|
interface PromptContext {
|
|
name: string;
|
|
description: string;
|
|
scope: "global" | "server";
|
|
server?: string | null;
|
|
categories: string[];
|
|
servers: string[];
|
|
}
|
|
|
|
let schemaSummaryCache: string | null = null;
|
|
|
|
async function loadSchemaSummary(): Promise<string> {
|
|
if (schemaSummaryCache) {
|
|
return schemaSummaryCache;
|
|
}
|
|
|
|
const schemaPath = path.join(process.cwd(), "prisma", "schema.prisma");
|
|
let file: string;
|
|
try {
|
|
file = await fs.readFile(schemaPath, "utf-8");
|
|
} catch (error) {
|
|
console.warn("[item-prompt] Não foi possível ler schema.prisma:", error);
|
|
schemaSummaryCache = "Schema indisponível.";
|
|
return schemaSummaryCache;
|
|
}
|
|
|
|
const modelRegex = /model\s+(\w+)\s*{([^}]*)}/g;
|
|
const blocks: string[] = [];
|
|
|
|
let match: RegExpExecArray | null;
|
|
while ((match = modelRegex.exec(file)) !== null) {
|
|
const modelName = match[1];
|
|
const body = match[2]
|
|
.split("\n")
|
|
.map((line) => line.trim())
|
|
.filter(
|
|
(line) => line && !line.startsWith("//") && !line.startsWith("@@"),
|
|
)
|
|
.join("\n");
|
|
|
|
blocks.push(`Model ${modelName} {\n${body}\n}`);
|
|
}
|
|
|
|
schemaSummaryCache = blocks.join("\n\n");
|
|
return schemaSummaryCache;
|
|
}
|
|
|
|
const FORM_FIELD_SUMMARY = `
|
|
Front-end fields (AdminItemForm):
|
|
- name: string
|
|
- category: string (utiliza categorias pré-carregadas)
|
|
- rarity: string opcional
|
|
- imageUrl / imageLocalPath: caminhos de imagem
|
|
- level, weight, slots: numéricos opcionais
|
|
- skill: string opcional
|
|
- servers: lista de servidores (separados por vírgula)
|
|
- tags: lista de tags (texto)
|
|
- description: texto (máximo 100 caracteres)
|
|
- dynamic metadata: campos variáveis definidos por categoria (servidores, atributos, etc)
|
|
`.trim();
|
|
|
|
export async function buildItemPrompt(
|
|
context: PromptContext,
|
|
extraInstructions?: string,
|
|
) {
|
|
const schemaSummary = await loadSchemaSummary();
|
|
|
|
const categoriesText =
|
|
context.categories.length > 0
|
|
? context.categories.map((category) => `- ${category}`).join("\n")
|
|
: "- Nenhuma categoria cadastrada";
|
|
|
|
const serversText =
|
|
context.servers.length > 0
|
|
? context.servers.map((server) => `- ${server}`).join("\n")
|
|
: "- Nenhum servidor cadastrado";
|
|
|
|
return `
|
|
Você é um assistente que prepara payloads JSON para cadastro de itens no banco de dados do Tibia Trade.
|
|
|
|
INSTRUÇÕES IMPORTANTES:
|
|
- Responda SOMENTE com JSON válido. Nenhum texto fora do JSON.
|
|
- Se não tiver certeza, retorne null nos campos desconhecidos.
|
|
- Utilize ao máximo a estrutura de tabelas e campos informada abaixo.
|
|
- Você receberá somente a estrutura das tabelas SQLite (nomes, colunas e tipos). Use esse esquema para escolher uma tabela existente adequada ou sugerir uma nova. Nunca inclua dados reais.
|
|
- Campo "description": limite máximo de 100 caracteres. Se a descrição original exceder 100 caracteres, retorne apenas os primeiros 100 caracteres sem acrescentar sufixos (sem "..."), e nunca gere texto além desse limite.
|
|
|
|
Contexto fornecido pelo usuário:
|
|
- Nome: ${context.name}
|
|
- Escopo: ${context.scope === "server" ? `servidor específico (${context.server ?? "não informado"})` : "global"}
|
|
- Descrição original:
|
|
"""
|
|
${context.description}
|
|
"""
|
|
- Categorias disponíveis no painel:
|
|
${categoriesText}
|
|
- Servidores disponíveis no painel:
|
|
${serversText}
|
|
|
|
Schema do banco (Prisma):
|
|
${schemaSummary}
|
|
|
|
Campos principais do formulário no front:
|
|
${FORM_FIELD_SUMMARY}
|
|
|
|
Formato obrigatório da resposta:
|
|
{
|
|
"table": "nome_da_tabela",
|
|
"columns": {
|
|
"name": string | null,
|
|
"description": string | null,
|
|
"category": string | null,
|
|
"rarity": string | null,
|
|
"image_url": string | null,
|
|
"image_local_path": string | null,
|
|
"level": number | null,
|
|
"weight": number | null,
|
|
"slots": number | null,
|
|
"skill": string | null,
|
|
"servers": string[] | null,
|
|
"tags": string[] | null,
|
|
"metadata": object | null
|
|
},
|
|
"schemaSuggestion": {
|
|
"table": "nome_da_tabela_existente_ou_nova",
|
|
"columns": [
|
|
{ "name": "coluna_1", "type": "TEXT|INTEGER|REAL|BOOLEAN" },
|
|
{ "name": "coluna_2", "type": "TEXT|INTEGER|REAL|BOOLEAN" }
|
|
]
|
|
}
|
|
}
|
|
|
|
Consistência:
|
|
- Escolha a tabela coerente com o escopo. Para global, use "items_all".
|
|
- Para escopo de servidor, use "items_<slug_do_servidor>", onde <slug> é o nome do servidor em minúsculas, com espaços transformados em "_" e removendo caracteres não alfanuméricos.
|
|
- Inclua sempre os campos básicos: name, description.
|
|
- Aplique outros campos conforme a descrição (ex.: atributos, requisitos, bônus).
|
|
- Se houver informações que encaixem em metadados dinâmicos, use um objeto "metadata".
|
|
- Nunca produza "description" com mais de 100 caracteres.
|
|
|
|
Regras adicionais:
|
|
- Servidores ("servers"): utilize SOMENTE nomes presentes na lista fornecida no contexto. Prefira o formato array de strings (ex.: ["Belobra", "Antica"]). Se não aplicável, use null.
|
|
- Metadata: inclua apenas atributos descritivos (ex.: "attributes", "requirements", "protections", "flags"). É PROIBIDO incluir qualquer campo de valor/preço/mercado. Não utilize chaves: "price", "value", "buy", "sell", "currency", "npc_price", "marketPrice", "minPrice", "maxPrice", "avgPrice".
|
|
- Tipos: números devem ser números (sem aspas). Se o valor for desconhecido, retorne null. Não invente dados.
|
|
|
|
${extraInstructions ?? ""}
|
|
`.trim();
|
|
}
|
|
|
|
export interface InlineImageData {
|
|
mimeType: string;
|
|
base64Data: string;
|
|
}
|
|
|
|
export async function buildGeminiParts(
|
|
prompt: string,
|
|
images: InlineImageData[] = [],
|
|
) {
|
|
const parts: Array<{
|
|
text?: string;
|
|
inline_data?: { mimeType: string; data: string };
|
|
}> = [{ text: prompt }];
|
|
|
|
images.slice(0, 2).forEach((image) => {
|
|
parts.push({
|
|
inline_data: {
|
|
mimeType: image.mimeType,
|
|
data: image.base64Data,
|
|
},
|
|
});
|
|
});
|
|
|
|
return parts;
|
|
}
|