feat(secretaria): roteamento por especialidade, situações, dúvida→humano, imagem, checklist do dono e desligar global
Secretária IA mais segura e contextual: - Roteamento por especialidade: /contexto (dentistas+especialidades) injetado no cérebro; descobrir o motivo e priorizar clínico geral só p/ avaliação/limpeza. - Situações da clínica: regras editáveis injetadas no prompt (siga à risca). - Dúvida/caso incerto → tool encaminhar_humano (não agenda; pede p/ aguardar). - Imagem/áudio: ext:message.transcription religa no fluxo ext:message.new (guard hadCaption evita processar 2x); a IA reage à descrição da imagem. - Checklist do dono × banco → cautelas no /contexto: NÃO assumir clínico geral por padrão p/ quem já é paciente; sem certeza de quem/qual dentista → humano. - Desligar GLOBAL da Secretária: GET/POST /secretaria/power (kill-switch auto_reply via config.set, efeito imediato, sem restart; visível a todos). Inclui a plumbing da sessão (chat.upsert, ownership de conexão, ws-bridge). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -395,6 +395,30 @@ 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 });
|
||||
});
|
||||
// ── ext:message.transcription → religa a Secretária p/ imagem/áudio ────────
|
||||
// Imagem/áudio não geram ext:message.new (não têm `body` de texto). Quando a
|
||||
// IA descreve a imagem / transcreve o áudio, reaproveitamos TODO o fluxo da
|
||||
// Secretária re-emitindo ext:message.new com o texto resultante. Imagem vira
|
||||
// um marcador "[Imagem: …]" (a IA identifica e, se for dúvida, encaminha ao
|
||||
// humano via encaminhar_humano); áudio entra como a própria fala transcrita.
|
||||
hooks.register('ext:message.transcription', async (data) => {
|
||||
const { tenantId, instanceId, chatId, jid, fromMe, hadCaption, contentType, transcription } = data || {};
|
||||
if (fromMe)
|
||||
return; // só mensagem RECEBIDA do cliente
|
||||
if (hadCaption)
|
||||
return; // legenda já disparou ext:message.new — não processar 2x
|
||||
if (!jid || !instanceId || !transcription?.trim())
|
||||
return;
|
||||
if (!String(jid).endsWith('@s.whatsapp.net'))
|
||||
return; // só 1:1
|
||||
const text = contentType === 'imageMessage'
|
||||
? `[Imagem enviada pelo cliente] ${transcription.trim()}`
|
||||
: transcription.trim();
|
||||
try {
|
||||
await hooks.emit('ext:message.new', { tenantId, instanceId, chatId, jid, text });
|
||||
}
|
||||
catch { /* best-effort */ }
|
||||
});
|
||||
// ── 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!".
|
||||
@@ -1939,6 +1963,30 @@ function buildExtRoutes(prisma, manager, db, config, hooks, io) {
|
||||
// expõem o contrato completo (agents/nodes/calendar/numbers/conversations)
|
||||
// esperado pelo frontend, reaproveitando a lógica dos /sec/*.
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// ── Liga/desliga GLOBAL da Secretária (kill-switch auto_reply) ───────────────
|
||||
// Desliga o auto-reply para TODOS os chats, sem timer — fica off até religarem
|
||||
// manualmente. Botão de segurança enquanto a IA amadurece; aberto a todos (por
|
||||
// ora). Efeito imediato: config.set atualiza o cache que o ext:message.new lê.
|
||||
router.get('/secretaria/power', async (_req, res) => {
|
||||
try {
|
||||
const cfg = (await config.get('secretaria'));
|
||||
res.json({ enabled: cfg?.auto_reply !== false });
|
||||
}
|
||||
catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
router.post('/secretaria/power', async (req, res) => {
|
||||
try {
|
||||
const enabled = !!(req.body?.enabled);
|
||||
const cfg = { ...(await config.get('secretaria')), auto_reply: enabled };
|
||||
await config.set('secretaria', cfg);
|
||||
res.json({ enabled });
|
||||
}
|
||||
catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
// ── Agents ──────────────────────────────────────────────────────────────────
|
||||
router.get('/secretaria/agents', async (_req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user