feat(secretaria): modelo de canal (clinica_id) + auto-reply 1:1 com nome legível
- sec_numbers ganha clinica_id (mapa canal→clínica); migrate idempotente. - clinica_id resolvido por CANAL no auto-reply (sec_numbers pela instância que recebeu) e por-REQUISIÇÃO no chat manual (header x-nw-clinica); brain.chat aceita opts.clinicaId e escopa a ponte de agenda (fallback: config global). - /secretaria/numbers CRUD inclui clinica_id. - auto-reply só cria conversa para 1:1 (@s.whatsapp.net) — exclui grupos/ broadcast/newsletter/lid; contact_name legível (Contact → +telefone → jid). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -103,8 +103,9 @@ function buildExtRoutes(prisma, manager, db, config, hooks, io) {
|
||||
const { tenantId, instanceId, chatId, jid, text } = data;
|
||||
if (!text?.trim())
|
||||
return;
|
||||
// NUNCA responde em grupos (@g.us): a Secretária é atendimento 1:1.
|
||||
if (jid.endsWith('@g.us'))
|
||||
// Só atendimento 1:1 (@s.whatsapp.net): exclui grupos (@g.us), broadcast,
|
||||
// newsletter, lid e status — a Secretária não cria conversa para esses.
|
||||
if (!jid.endsWith('@s.whatsapp.net'))
|
||||
return;
|
||||
// Kill-switch do auto-reply: secretaria.auto_reply === false desliga SÓ o
|
||||
// disparo automático da Secretária (envios manuais seguem normais). Só
|
||||
@@ -122,10 +123,16 @@ function buildExtRoutes(prisma, manager, db, config, hooks, io) {
|
||||
const agent = await db('sec_agents').where({ active: true }).orderBy('created_at').first();
|
||||
if (!agent)
|
||||
return;
|
||||
// Nome legível: Contact (agenda/WhatsApp) → telefone formatado → jid.
|
||||
const contact = await prisma.contact.findFirst({
|
||||
where: { jid, instanceId }, select: { name: true, verifiedName: true, notify: true },
|
||||
});
|
||||
const phone = jid.split('@')[0];
|
||||
const displayName = contact?.name ?? contact?.verifiedName ?? contact?.notify ?? (phone ? `+${phone}` : jid);
|
||||
const [newConv] = await db('sec_conversations').insert({
|
||||
id: uuid(),
|
||||
agent_id: agent.id,
|
||||
contact_name: jid,
|
||||
contact_name: displayName,
|
||||
protocol_number: brain_1.ProtocolEngine.generateProtocolNumber(),
|
||||
status: 'active',
|
||||
ext_chat_id: extKey,
|
||||
@@ -138,8 +145,11 @@ function buildExtRoutes(prisma, manager, db, config, hooks, io) {
|
||||
// SEC-14: indicador de digitação antes de chamar a IA
|
||||
const sock = manager?.getSocket(instanceId);
|
||||
await sock?.sendPresenceUpdate('composing', jid).catch(() => { });
|
||||
// Modelo de canal: a clínica vem do NÚMERO que recebeu a mensagem
|
||||
// (sec_numbers.clinica_id da instância) — escopo da ponte de agenda.
|
||||
const channel = await db('sec_numbers').where({ instance_id: instanceId }).first();
|
||||
const brain = new brain_1.ProtocolEngine(db, config);
|
||||
const reply = await brain.chat(conv.id, text.trim(), { tenantId, hooks });
|
||||
const reply = await brain.chat(conv.id, text.trim(), { tenantId, hooks, clinicaId: channel?.clinica_id });
|
||||
await sock?.sendPresenceUpdate('paused', jid).catch(() => { });
|
||||
await sendSecretariaReply({ instanceId, tenantId, chatId, jid, reply });
|
||||
});
|
||||
@@ -1036,8 +1046,9 @@ function buildExtRoutes(prisma, manager, db, config, hooks, io) {
|
||||
try {
|
||||
const b = req.body ?? {};
|
||||
const [n] = await db('sec_numbers').insert({
|
||||
id: uuid(), instance_id: b.instance_id ?? null, phone: b.phone ?? null, label: b.label ?? null,
|
||||
role: b.role ?? null, area: b.area ?? null, priority: b.priority ?? 0, active: b.active ?? true, notes: b.notes ?? null,
|
||||
id: uuid(), instance_id: b.instance_id ?? null, clinica_id: b.clinica_id ?? null,
|
||||
phone: b.phone ?? null, label: b.label ?? b.phone ?? 'Número',
|
||||
role: b.role ?? 'clinic', area: b.area ?? null, priority: b.priority ?? 10, active: b.active ?? true, notes: b.notes ?? null,
|
||||
}).returning('*');
|
||||
res.status(201).json(n);
|
||||
}
|
||||
@@ -1048,7 +1059,7 @@ function buildExtRoutes(prisma, manager, db, config, hooks, io) {
|
||||
router.put('/secretaria/numbers/:id', async (req, res) => {
|
||||
try {
|
||||
const b = req.body ?? {}, patch = { updated_at: new Date() };
|
||||
for (const k of ['instance_id', 'phone', 'label', 'role', 'area', 'priority', 'active', 'notes'])
|
||||
for (const k of ['instance_id', 'clinica_id', 'phone', 'label', 'role', 'area', 'priority', 'active', 'notes'])
|
||||
if (k in b)
|
||||
patch[k] = b[k];
|
||||
const [n] = await db('sec_numbers').where({ id: req.params['id'] }).update(patch).returning('*');
|
||||
@@ -1134,8 +1145,10 @@ function buildExtRoutes(prisma, manager, db, config, hooks, io) {
|
||||
res.status(400).json({ error: 'message obrigatório' });
|
||||
return;
|
||||
}
|
||||
// clínica por-requisição: workspace ativo do satélite (header x-nw-clinica).
|
||||
const clinicaId = req.headers['x-nw-clinica']?.trim() || undefined;
|
||||
const brain = new brain_1.ProtocolEngine(db, config);
|
||||
const reply = await brain.chat(req.params['id'], message, { tenantId: req.extTenantId, hooks });
|
||||
const reply = await brain.chat(req.params['id'], message, { tenantId: req.extTenantId, hooks, clinicaId });
|
||||
res.json({ reply });
|
||||
}
|
||||
catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user