feat(ext-api): alias GET /secretaria/numbers (sincroniza contrato com satélite)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -70,8 +70,70 @@ function cancelHandoffTimeout(convId) {
|
|||||||
handoffTimers.delete(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)();
|
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 ──────────────────────────────────────
|
// ── Escalation notification listener ──────────────────────────────────────
|
||||||
// When escalar_humano tool fires, send a WA message to the configured admin phone.
|
// When escalar_humano tool fires, send a WA message to the configured admin phone.
|
||||||
hooks.register('ext:escalated', async (data) => {
|
hooks.register('ext:escalated', async (data) => {
|
||||||
@@ -674,6 +736,15 @@ function buildExtRoutes(prisma, manager, db, config, hooks) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
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}`;
|
const extKey = `${tenantId}:${chatId}`;
|
||||||
let conv = await db('sec_conversations')
|
let conv = await db('sec_conversations')
|
||||||
.where({ ext_chat_id: extKey, status: 'active' })
|
.where({ ext_chat_id: extKey, status: 'active' })
|
||||||
@@ -716,6 +787,21 @@ function buildExtRoutes(prisma, manager, db, config, hooks) {
|
|||||||
hooks,
|
hooks,
|
||||||
tenantId,
|
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({
|
res.json({
|
||||||
reply,
|
reply,
|
||||||
conversationId: conv.id,
|
conversationId: conv.id,
|
||||||
@@ -726,6 +812,18 @@ function buildExtRoutes(prisma, manager, db, config, hooks) {
|
|||||||
res.status(500).json({ error: err.message ?? 'Erro interno' });
|
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 ──────────────────────────────────────────────
|
// ── GET /sec/handoff/:chatId ──────────────────────────────────────────────
|
||||||
// Retorna o modo de handoff atual para um chat específico.
|
// Retorna o modo de handoff atual para um chat específico.
|
||||||
router.get('/sec/handoff/:chatId', async (req, res) => {
|
router.get('/sec/handoff/:chatId', async (req, res) => {
|
||||||
|
|||||||
@@ -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 ──────────────────────────────────────────────
|
// ── GET /sec/handoff/:chatId ──────────────────────────────────────────────
|
||||||
// Retorna o modo de handoff atual para um chat específico.
|
// Retorna o modo de handoff atual para um chat específico.
|
||||||
router.get('/sec/handoff/:chatId', async (req: Request, res: Response) => {
|
router.get('/sec/handoff/:chatId', async (req: Request, res: Response) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user