feat(secretaria): follow-up proativo com atraso (Parte B)
Quando o cliente insiste num horário mais tarde, a IA usa confirmar_depois: diz "vou confirmar, aguarde uns minutinhos" e registra um follow-up (sec_agenda_followups, fire_at = now + followup_delay_min, default 15). Um worker no ext-api (a cada 30s) processa os vencidos: consulta /slots perto do horário desejado e manda ao paciente "Oie, consegui! Posso às X?" (assinado pelo agente). Testado em dev: worker processa follow-up vencido, consulta a agenda e envia. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -354,6 +354,53 @@ function buildExtRoutes(prisma, manager, db, config, hooks, io) {
|
||||
const secAgent = await db('sec_agents').where({ id: conv.agent_id }).select('name').first();
|
||||
await sendSecretariaReply({ instanceId, tenantId, chatId, jid, reply, agentName: secAgent?.name });
|
||||
});
|
||||
// ── Worker: follow-ups proativos da agenda (Parte B) ──────────────────────
|
||||
// A cada 30s processa os follow-ups vencidos (confirmar_depois): consulta a
|
||||
// agenda perto do horário desejado e manda ao paciente "Oie, consegui!".
|
||||
setInterval(async () => {
|
||||
let due = [];
|
||||
try {
|
||||
due = await db('sec_agenda_followups').where('status', 'pendente').where('fire_at', '<=', new Date()).limit(10);
|
||||
}
|
||||
catch {
|
||||
return;
|
||||
}
|
||||
for (const fu of due) {
|
||||
try {
|
||||
const claimed = await db('sec_agenda_followups').where({ id: fu.id, status: 'pendente' }).update({ status: 'processando' });
|
||||
if (!claimed)
|
||||
continue;
|
||||
const dataStr = fu.data ? (fu.data instanceof Date ? fu.data.toISOString().slice(0, 10) : String(fu.data).slice(0, 10)) : null;
|
||||
const u = new URL(`${String(fu.agenda_url).replace(/\/$/, '')}/slots`);
|
||||
u.searchParams.set('clinica_id', fu.clinica_id);
|
||||
if (dataStr)
|
||||
u.searchParams.set('date', dataStr);
|
||||
if (fu.periodo)
|
||||
u.searchParams.set('periodo', fu.periodo);
|
||||
const r = await fetch(u, { headers: { 'x-nw-agenda-secret': fu.agenda_secret || '' }, signal: AbortSignal.timeout(12000) });
|
||||
const j = r.ok ? await r.json() : {};
|
||||
const slots = j?.slots ?? [];
|
||||
// slot mais próximo do horário desejado (senão o primeiro).
|
||||
let escolhido = slots[0];
|
||||
if (fu.hora_desejada && slots.length) {
|
||||
const alvo = String(fu.hora_desejada).slice(0, 5);
|
||||
escolhido = slots.slice().sort((a, b) => Math.abs(a.start.slice(11, 16).localeCompare(alvo)) - Math.abs(b.start.slice(11, 16).localeCompare(alvo)))[0] || slots[0];
|
||||
}
|
||||
const texto = escolhido
|
||||
? `Oie! Consegui aqui 😊 Posso te agendar${dataStr ? ` dia ${dataStr.slice(8, 10)}/${dataStr.slice(5, 7)}` : ''} às ${escolhido.start.slice(11, 16)}. Fica bom pra você?`
|
||||
: 'Oi! Verifiquei a agenda e não consegui esse horário 😕 Posso te oferecer outro dia ou período?';
|
||||
// assina com o agente do número (channel) para manter o padrão.
|
||||
const ch = await db('sec_numbers').where({ instance_id: fu.instance_id }).first();
|
||||
const ag = ch?.agent_id ? await db('sec_agents').where({ id: ch.agent_id }).first() : await db('sec_agents').where({ active: true }).orderBy('created_at').first();
|
||||
const chat = await prisma.chat.findFirst({ where: { instanceId: fu.instance_id, jid: fu.jid }, orderBy: { updatedAt: 'desc' }, select: { id: true } });
|
||||
await sendSecretariaReply({ instanceId: fu.instance_id, tenantId: fu.tenant_id, chatId: chat?.id ?? fu.jid, jid: fu.jid, reply: texto, agentName: ag?.name });
|
||||
await db('sec_agenda_followups').where({ id: fu.id }).update({ status: 'enviado' });
|
||||
}
|
||||
catch {
|
||||
await db('sec_agenda_followups').where({ id: fu.id }).update({ status: 'falhou' }).catch(() => { });
|
||||
}
|
||||
}
|
||||
}, 30000);
|
||||
// ── Escalation notification listener ──────────────────────────────────────
|
||||
// When escalar_humano tool fires, send a WA message to the configured admin phone.
|
||||
hooks.register('ext:escalated', async (data) => {
|
||||
|
||||
Reference in New Issue
Block a user