chore(ops): restore all source files in newwhats.clube67.com
continuous-integration/webhook Falha no deploy de clube67_newwhats.local (VPS 4)

This commit is contained in:
VPS 4 Deploy Agent
2026-05-18 03:28:29 +02:00
parent 52f73753e7
commit 2f8c04a0a7
755 changed files with 466910 additions and 0 deletions
@@ -0,0 +1,290 @@
import { Knex } from 'knex'
export async function runMigrations(db: Knex): Promise<void> {
// ── sec_agents ────────────────────────────────────────────────────────────
if (!(await db.schema.hasTable('sec_agents'))) {
await db.schema.createTable('sec_agents', (t) => {
t.uuid('id').primary().defaultTo(db.raw('gen_random_uuid()'))
t.string('name', 100).notNullable()
t.text('description').nullable()
t.string('model', 60).defaultTo('gpt-4o-mini')
t.string('provider', 20).defaultTo('openai')
t.float('temperature').defaultTo(0.7)
t.integer('context_window').defaultTo(4)
t.boolean('active').defaultTo(true)
t.timestamps(true, true)
})
}
// ── sec_brain_nodes ───────────────────────────────────────────────────────
if (!(await db.schema.hasTable('sec_brain_nodes'))) {
await db.schema.createTable('sec_brain_nodes', (t) => {
t.uuid('id').primary().defaultTo(db.raw('gen_random_uuid()'))
t.uuid('agent_id').notNullable().references('id').inTable('sec_agents').onDelete('CASCADE')
t.string('type', 30).notNullable() // persona | knowledge | rules | calendar | escalation
t.string('title', 100).notNullable()
t.text('content').notNullable()
t.boolean('active').defaultTo(true)
t.integer('sort_order').defaultTo(0)
t.timestamps(true, true)
})
}
// ── sec_conversations ─────────────────────────────────────────────────────
if (!(await db.schema.hasTable('sec_conversations'))) {
await db.schema.createTable('sec_conversations', (t) => {
t.uuid('id').primary().defaultTo(db.raw('gen_random_uuid()'))
t.uuid('agent_id').notNullable().references('id').inTable('sec_agents').onDelete('CASCADE')
t.string('contact_name', 100).defaultTo('Usuário')
t.string('protocol_number', 20).notNullable().defaultTo('') // DDMMYYHHmmSS ex: 120426224935
t.string('status', 20).defaultTo('active') // active | closed | escalated
t.text('summary').nullable() // resumo compacto (economia de tokens)
t.timestamps(true, true)
})
}
// ── sec_conversations — adiciona protocol_number se ainda não existe ───────
if (await db.schema.hasTable('sec_conversations')) {
if (!(await db.schema.hasColumn('sec_conversations', 'protocol_number'))) {
await db.schema.alterTable('sec_conversations', (t) => {
t.string('protocol_number', 20).notNullable().defaultTo('')
})
}
}
// ── sec_messages ──────────────────────────────────────────────────────────
if (!(await db.schema.hasTable('sec_messages'))) {
await db.schema.createTable('sec_messages', (t) => {
t.uuid('id').primary().defaultTo(db.raw('gen_random_uuid()'))
t.uuid('conversation_id').notNullable().references('id').inTable('sec_conversations').onDelete('CASCADE')
t.string('role', 20).notNullable() // user | assistant | system
t.text('content').notNullable()
t.timestamps(true, true)
})
}
// ── sec_brain_nodes — adiciona node_model se ainda não existe ────────────
if (await db.schema.hasTable('sec_brain_nodes')) {
if (!(await db.schema.hasColumn('sec_brain_nodes', 'node_model'))) {
await db.schema.alterTable('sec_brain_nodes', (t) => {
t.string('node_model', 80).nullable()
})
}
}
// ── sec_conversations — adiciona ext_chat_id para integração ext-api ─────
// Chave: "<tenantId>:<chatId>" — identifica a conversa por canal externo
if (await db.schema.hasTable('sec_conversations')) {
if (!(await db.schema.hasColumn('sec_conversations', 'ext_chat_id'))) {
await db.schema.alterTable('sec_conversations', (t) => {
t.string('ext_chat_id', 300).nullable()
})
await db.raw('CREATE INDEX IF NOT EXISTS idx_sec_conv_ext_chat ON sec_conversations (ext_chat_id)')
}
}
// ── sec_conversations — handoff mode (ia | humano) ────────────────────────
if (await db.schema.hasTable('sec_conversations')) {
if (!(await db.schema.hasColumn('sec_conversations', 'handoff_mode'))) {
await db.schema.alterTable('sec_conversations', (t) => {
t.string('handoff_mode', 20).notNullable().defaultTo('ia')
})
}
if (!(await db.schema.hasColumn('sec_conversations', 'handoff_human_at'))) {
await db.schema.alterTable('sec_conversations', (t) => {
t.timestamp('handoff_human_at').nullable()
})
}
}
// ── sec_calendar ──────────────────────────────────────────────────────────
if (!(await db.schema.hasTable('sec_calendar'))) {
await db.schema.createTable('sec_calendar', (t) => {
t.uuid('id').primary().defaultTo(db.raw('gen_random_uuid()'))
t.string('title', 200).notNullable()
t.date('date').notNullable()
t.time('time_start').notNullable()
t.time('time_end').notNullable()
t.string('attendee_name', 100).nullable()
t.string('attendee_phone', 30).nullable()
t.string('status', 20).defaultTo('available') // available | booked | cancelled
t.text('notes').nullable()
t.timestamps(true, true)
})
}
// ── sec_numbers ───────────────────────────────────────────────────────────
if (!(await db.schema.hasTable('sec_numbers'))) {
await db.schema.createTable('sec_numbers', (t) => {
t.uuid('id').primary().defaultTo(db.raw('gen_random_uuid()'))
t.string('instance_id', 100).nullable() // ID da instância Baileys existente
t.string('phone', 30).nullable() // número do WhatsApp (preenchido após conexão)
t.string('label', 100).notNullable() // apelido (pode vir do nome da instância)
t.string('role', 30).notNullable().defaultTo('clinic') // secretary_virtual | clinic | doctor | specialist | manager | reserve | human_secretary
t.string('area', 100).nullable() // área de responsabilidade
t.integer('priority').defaultTo(10) // menor = maior prioridade no fallback
t.boolean('active').defaultTo(true)
t.text('notes').nullable()
t.timestamps(true, true)
})
}
// ── Seeds: agente padrão + nós + calendário ───────────────────────────────
const agentCount = await db('sec_agents').count('id as c').first()
if (Number(agentCount?.c ?? 0) === 0) {
await seedDefaults(db)
}
}
async function seedDefaults(db: Knex): Promise<void> {
// Agente padrão
const [agent] = await db('sec_agents').insert({
name: 'Ana — Atendente Virtual',
description: 'Atendente geral para suporte, financeiro, dúvidas e agendamentos',
model: 'gemini-2.0-flash',
provider: 'gemini',
temperature: 0.7,
context_window: 10,
active: true,
}).returning('id')
const agentId = typeof agent === 'object' ? agent.id : agent
// Nós do cérebro
await db('sec_brain_nodes').insert([
{
agent_id: agentId,
type: 'persona',
title: 'Identidade — Ana',
content: `Você é Ana, atendente da empresa. Seu jeito de ser: calorosa, paciente e direta — nunca robótica.
Adapte seu tom ao do cliente: se ele for informal e descontraído, seja assim também; se for mais sério e objetivo, acompanhe. Leia o ritmo dele.
Ao longo da conversa, use o nome do cliente de forma natural — não em toda mensagem, só quando fizer sentido, como uma pessoa real faria.
Quando o cliente estiver frustrado ou irritado, reconheça o sentimento antes de tentar resolver. Dizer "Entendo, isso é realmente chato" antes de dar a solução faz toda a diferença.
Varie suas respostas. Nunca use a mesma frase de confirmação duas vezes seguidas. Nunca use linguagem corporativa: esqueça "atenciosamente", "prezado cliente", "informamos que" e afins.
Quando não souber algo, seja honesta: "Deixa eu verificar isso com mais calma pra te dar uma resposta certa."`,
active: true,
sort_order: 0,
},
{
agent_id: agentId,
type: 'rules',
title: 'Regras de Comportamento',
content: `1. Cumprimente o cliente pelo nome quando já souber — mas de forma natural, não mecânica.
2. Nunca revele que é uma IA a menos que perguntado diretamente. Se perguntado, responda com honestidade e leveza.
3. Para questões financeiras que você não consegue resolver, ofereça encaminhar para um especialista.
4. Respostas curtas e diretas em geral. Só detalhe mais quando o assunto exigir — não encha de texto desnecessário.
5. Antes de confirmar um agendamento, valide: data, horário e nome completo. Faça isso de forma conversacional, não como checklist.
6. Nunca repita a mesma frase de confirmação em sequência. Varie: "Faz sentido!", "Entendi sim.", "Tudo certo.", "Combinado.", etc.
7. Se o cliente mandar uma mensagem muito longa ou confusa, foque no ponto principal e pergunte apenas o que for essencial.`,
active: true,
sort_order: 1,
},
{
agent_id: agentId,
type: 'rules',
title: 'Inteligência Emocional',
content: `Quando o cliente demonstrar frustração ou raiva:
- Primeiro reconheça: "Entendo, isso é realmente frustrante." ou "Faz todo sentido ficar chateado com isso."
- Só depois vá para a solução. Nunca pule direto para a resposta técnica quando o cliente está emotivo.
Quando o cliente estiver com urgência:
- Responda de forma objetiva e sem enrolação. Priorize resolver.
Quando o cliente agradecer ou elogiar:
- Responda de forma genuína e breve. Evite "Disponha! Qualquer coisa é só chamar." — prefira algo como "Fico feliz que resolveu!" ou "Que bom, até mais!"
Quando o cliente disser que vai cancelar ou está insatisfeito:
- Não entre em modo de venda forçada. Ouça o motivo, reconheça, e só então — se fizer sentido — apresente alternativas.`,
active: true,
sort_order: 2,
},
{
agent_id: agentId,
type: 'knowledge',
title: 'Base de Conhecimento',
content: `Horário de atendimento: Segunda a Sexta, 08h às 18h. Suporte emergencial 24h.
Contatos: WhatsApp (11) 99999-9999 | Email: suporte@empresa.com
⚠️ Atenção: substitua estas informações pelas reais da sua empresa antes de usar em produção.
Planos disponíveis:
• Básico: R$ 99/mês — até 2 usuários, 1 instância WhatsApp
• Pro: R$ 199/mês — até 10 usuários, 5 instâncias
• Enterprise: sob consulta com a equipe comercial
SLA de suporte: Crítico 2h | Alta 8h | Normal 24h
Política de cancelamento: aviso prévio de 30 dias por email.`,
active: true,
sort_order: 3,
},
{
agent_id: agentId,
type: 'calendar',
title: 'Acesso à Agenda',
content: `Você tem acesso à agenda da empresa e pode consultar horários disponíveis.
Quando o cliente mencionar agendamento, consulte os horários e ofereça opções concretas — não peça que ele escolha sem saber o que está disponível.
Ao confirmar um agendamento, repita o resumo de forma natural: "Então ficou marcado para [data] às [hora], certo? Vou registrar aqui."`,
active: true,
sort_order: 4,
},
{
agent_id: agentId,
type: 'escalation',
title: 'Quando Escalar para Humano',
content: `Transfira o atendimento para um humano quando:
- O cliente pedir explicitamente falar com uma pessoa
- O problema envolver contestação de cobrança, estorno ou situação financeira sensível
- O cliente demonstrar raiva intensa por mais de 2 mensagens seguidas, sem que você consiga ajudar
- Você não souber responder após 2 tentativas honestas
Como escalar de forma natural:
Não diga "vou transferir você". Prefira: "Vou chamar a [nome/equipe] que consegue resolver isso melhor pra você. Um momento?" — e aguarde confirmação antes de encerrar.
Ao escalar, registre brevemente o contexto para quem vai assumir: o nome do cliente, o problema e o que já foi tentado.`,
active: true,
sort_order: 5,
},
])
// Calendário — próximos 7 dias com slots de teste
const slots: any[] = []
const types = ['Consulta Técnica', 'Reunião de Onboarding', 'Demonstração do Produto', 'Suporte Premium']
const times = [
{ s: '09:00', e: '10:00' },
{ s: '10:00', e: '11:00' },
{ s: '11:00', e: '12:00' },
{ s: '14:00', e: '15:00' },
{ s: '15:00', e: '16:00' },
{ s: '16:00', e: '17:00' },
]
for (let d = 1; d <= 7; d++) {
const date = new Date()
date.setDate(date.getDate() + d)
// Skip weekends
if (date.getDay() === 0 || date.getDay() === 6) continue
const dateStr = date.toISOString().split('T')[0]
times.forEach((t, idx) => {
const isBooked = (d === 1 && idx === 2) || (d === 2 && idx === 4) || (d === 4 && idx === 1)
slots.push({
title: types[idx % types.length],
date: dateStr,
time_start: t.s,
time_end: t.e,
status: isBooked ? 'booked' : 'available',
attendee_name: isBooked ? ['João Silva', 'Maria Santos', 'Carlos Lima'][d % 3] : null,
attendee_phone: isBooked ? `119${String(d * 1111 + idx * 100).padStart(8, '0')}` : null,
})
})
}
if (slots.length > 0) {
await db('sec_calendar').insert(slots)
}
}