'use strict' /** * sync-knowledge.js * Constrói o nó de conhecimento da Secretária IA com dados reais do banco * e sincroniza com o motor NewWhats via API REST. * * Fluxo: * 1. Lê dados do DB Alemão (clubes, planos, parceiros, promoções, sorteio, vagas) * 2. Monta texto estruturado do knowledge node * 3. Chama o motor NewWhats para atualizar o nó (ou cria se não existir) * 4. Retorna { ok, preview, agent_id, node_id } */ const { query, queryOne } = require('../../database/postgres') // ── Busca configurações do plugin no banco ──────────────────────────────────── async function getPluginConfig() { const rows = await query( `SELECT key, value FROM plugin_configs WHERE plugin_id = 'newwhats'` ) const cfg = {} for (const r of rows) cfg[r.key] = r.value return cfg } // ── Chama a API interna do motor NewWhats (/api/secretaria/*) ───────────────── // Nota: as rotas /api/secretaria/* são internas do motor (não passam pelo proxy // /api/ext/v1). São acessadas diretamente pela URL do motor configurada no plugin. async function motorFetch(motorUrl, _integKey, method, path, body) { const url = `${motorUrl}/api/secretaria${path}` const opts = { method, headers: { 'Content-Type': 'application/json' }, } if (body) opts.body = JSON.stringify(body) const res = await fetch(url, opts) if (!res.ok) { const txt = await res.text() throw new Error(`Motor ${method} /api/secretaria${path} → ${res.status}: ${txt}`) } return res.json() } // ── Monta o texto do knowledge node ────────────────────────────────────────── async function buildKnowledgeText() { const lines = [] // ── Empresa ────────────────────────────────────────────────────────────── lines.push(`=== ALEMÃO CONVENIÊNCIAS ===`) lines.push(`Somos uma empresa de conveniências com foco em benefícios para nossos clientes.`) lines.push(`Site: alemaoconveniencias.com | Atendimento via WhatsApp`) lines.push(``) // ── Clubes de Benefícios ───────────────────────────────────────────────── const clubes = await query( `SELECT id, name, slug, specialty, conditions, rules FROM benefit_clubs WHERE active = true ORDER BY id ASC` ) if (clubes.length) { lines.push(`=== CLUBES DE BENEFÍCIOS ===`) for (const clube of clubes) { lines.push(`\n• ${clube.name} (${clube.specialty})`) // Planos do clube const planos = await query( `SELECT name, price_monthly, max_dependents, features FROM club_plans WHERE club_id = $1 AND active = true ORDER BY price_monthly ASC`, [clube.id] ) if (planos.length) { lines.push(` Planos disponíveis:`) for (const p of planos) { const deps = p.max_dependents > 0 ? ` (até ${p.max_dependents} dependente${p.max_dependents > 1 ? 's' : ''})` : ' (titular)' lines.push(` - ${p.name}${deps}: R$ ${Number(p.price_monthly).toFixed(2).replace('.', ',')}/mês`) let features = [] try { features = typeof p.features === 'string' ? JSON.parse(p.features) : (p.features || []) } catch {} if (features.length) lines.push(` Incluído: ${features.slice(0, 4).join(', ')}`) } } // Parceiros do clube const parceiros = await query( `SELECT name, specialty, phone, address, discount_percent FROM club_partners WHERE club_id = $1 AND active = true ORDER BY name ASC LIMIT 8`, [clube.id] ) if (parceiros.length) { lines.push(` Parceiros credenciados:`) for (const p of parceiros) { const desc = p.discount_percent ? ` — ${p.discount_percent}% desconto` : '' const tel = p.phone ? ` | Tel: ${p.phone}` : '' lines.push(` - ${p.name} (${p.specialty})${desc}${tel}`) if (p.address) lines.push(` Endereço: ${p.address}`) } } if (clube.conditions) lines.push(` Condições: ${clube.conditions}`) if (clube.rules) lines.push(` Regras: ${clube.rules}`) } lines.push(``) } // ── Sorteio Ativo ───────────────────────────────────────────────────────── const sorteio = await queryOne( `SELECT titulo, descricao, preco_numero, total_numeros FROM raffles WHERE ativo = 1 LIMIT 1` ) if (sorteio) { const countRow = await queryOne( `SELECT COUNT(*) AS total FROM tickets t JOIN raffles r ON r.id = t.raffle_id WHERE r.ativo = 1 AND (t.status = 'sold' OR (t.status = 'reserved' AND t.reserved_until > NOW()))` ) const vendidos = parseInt(countRow?.total ?? 0, 10) const disponiveis = sorteio.total_numeros - vendidos lines.push(`=== SORTEIO ATIVO ===`) lines.push(`Nome: ${sorteio.titulo}`) if (sorteio.descricao) lines.push(`Descrição: ${sorteio.descricao}`) lines.push(`Preço por número: R$ ${Number(sorteio.preco_numero).toFixed(2).replace('.', ',')}`) lines.push(`Números disponíveis: ${disponiveis} de ${sorteio.total_numeros}`) lines.push(`Como comprar: acesse nosso site e escolha seus números favoritos.`) lines.push(``) } // ── Promoções Ativas ────────────────────────────────────────────────────── const promos = await query( `SELECT nome, descricao, preco_original, preco_promocional FROM promotions WHERE ativo = TRUE ORDER BY id ASC` ) if (promos.length) { lines.push(`=== PROMOÇÕES ATIVAS ===`) for (const p of promos) { const economia = p.preco_original && p.preco_promocional ? ` (de R$ ${Number(p.preco_original).toFixed(2).replace('.', ',')} por R$ ${Number(p.preco_promocional).toFixed(2).replace('.', ',')})` : '' lines.push(`• ${p.nome}${economia}`) if (p.descricao) lines.push(` ${p.descricao}`) } lines.push(``) } // ── Vagas de Emprego ────────────────────────────────────────────────────── const vagas = await query( `SELECT title, category, work_mode, job_type, location, salary_hide, salary_min, salary_max FROM jobs WHERE active = true AND (expires_at IS NULL OR expires_at > NOW()) ORDER BY published_at DESC LIMIT 10` ) if (vagas.length) { const modeMap = { presencial: 'Presencial', hibrido: 'Híbrido', remoto: 'Remoto' } const typeMap = { clt: 'CLT', pj: 'PJ', freelance: 'Freelance', estagio: 'Estágio', temporario: 'Temporário' } lines.push(`=== VAGAS DE EMPREGO DISPONÍVEIS ===`) lines.push(`(Temos ${vagas.length} vaga${vagas.length > 1 ? 's' : ''} abertas no momento)`) for (const v of vagas) { const modo = modeMap[v.work_mode] || v.work_mode const tipo = typeMap[v.job_type] || v.job_type const sal = v.salary_hide || (!v.salary_min && !v.salary_max) ? 'A combinar' : v.salary_min && v.salary_max ? `R$ ${Number(v.salary_min).toLocaleString('pt-BR', { maximumFractionDigits: 0 })}–${Number(v.salary_max).toLocaleString('pt-BR', { maximumFractionDigits: 0 })}` : v.salary_min ? `A partir de R$ ${Number(v.salary_min).toLocaleString('pt-BR', { maximumFractionDigits: 0 })}` : `Até R$ ${Number(v.salary_max).toLocaleString('pt-BR', { maximumFractionDigits: 0 })}` const loc = v.location ? ` | ${v.location}` : '' lines.push(`• ${v.title} — ${modo} | ${tipo}${loc} | ${sal}`) } lines.push(`Para se candidatar, acesse nosso site na seção Vagas.`) lines.push(``) } // ── Informações Gerais ──────────────────────────────────────────────────── lines.push(`=== ATENDIMENTO ===`) lines.push(`Horário: Consulte pelo WhatsApp ou site para horário atualizado.`) lines.push(`Carteirinha digital: disponível na área do cliente em nosso site.`) lines.push(`Dúvidas sobre pagamento, cancelamento ou benefícios: informe que vai encaminhar para nossa equipe.`) return lines.join('\n') } // ── Endpoint principal: sincroniza knowledge node no motor ──────────────────── async function syncKnowledge() { const cfg = await getPluginConfig() const motorUrl = cfg.newwhats_url const integKey = cfg.integration_key if (!motorUrl || !integKey) { throw new Error('Plugin NewWhats não configurado (newwhats_url ou integration_key ausentes)') } // 1. Monta o texto const knowledgeText = await buildKnowledgeText() // 2. Busca agentes no motor const agents = await motorFetch(motorUrl, integKey, 'GET', '/agents') if (!agents.length) throw new Error('Nenhum agente encontrado no motor. Crie um agente na Secretária IA.') const agent = agents.find(a => a.active) || agents[0] // 3. Busca nós do agente const nodes = await motorFetch(motorUrl, integKey, 'GET', `/agents/${agent.id}/nodes`) // 4. Encontra o nó de knowledge (ou cria) const knowledgeNode = nodes.find(n => n.type === 'knowledge') let nodeId if (knowledgeNode) { // Atualiza nó existente await motorFetch(motorUrl, integKey, 'PUT', `/nodes/${knowledgeNode.id}`, { title: 'Base de Conhecimento — Alemão Conveniências', content: knowledgeText, active: true, sort_order: knowledgeNode.sort_order ?? 3, }) nodeId = knowledgeNode.id } else { // Cria nó novo const created = await motorFetch(motorUrl, integKey, 'POST', `/agents/${agent.id}/nodes`, { type: 'knowledge', title: 'Base de Conhecimento — Alemão Conveniências', content: knowledgeText, sort_order: 3, }) nodeId = created.id } return { ok: true, agent_id: agent.id, agent: agent.name, node_id: nodeId, preview: knowledgeText, chars: knowledgeText.length, synced_at: new Date().toISOString(), } } module.exports = { syncKnowledge, buildKnowledgeText }