feat(secretaria): monitor de cota dos providers + notificação ao admin
- brain: detecta 429/cota (quota/billing/insufficient/exceeded/limite da api/ rate limit — inclui a msg PT do Gemini) e emite ext:provider.exhausted por provider da chain (throttle de 6h por provider), com fallback informado. - routes: handler ext:provider.exhausted monta notificação profissional e envia por WhatsApp ao admin (admin_notify_phone da config secretaria), resolvendo o JID canônico via onWhatsApp (trata 9º dígito BR; pula se número não existe). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -182,6 +182,60 @@ function buildExtRoutes(prisma, manager, db, config, hooks, io) {
|
||||
}
|
||||
catch { /* notificação é best-effort */ }
|
||||
});
|
||||
// ── Monitor de cota — provedor LLM sem saldo → notifica o admin ────────────
|
||||
// Emitido pelo brain quando um provider retorna 429 (cota/crédito esgotado).
|
||||
const PROVIDER_LABEL = {
|
||||
openai: 'OpenAI', gemini: 'Google Gemini', anthropic: 'Anthropic', ollama: 'Ollama (local)',
|
||||
};
|
||||
hooks.register('ext:provider.exhausted', async (data) => {
|
||||
try {
|
||||
const prov = PROVIDER_LABEL[data.provider] ?? data.provider;
|
||||
const fb = Array.isArray(data.fallbackTo) && data.fallbackTo.length
|
||||
? data.fallbackTo.map((p) => PROVIDER_LABEL[p] ?? p).join(' → ')
|
||||
: 'nenhum (todos esgotados)';
|
||||
const when = new Date(data.at ?? Date.now()).toLocaleString('pt-BR', { dateStyle: 'short', timeStyle: 'short' });
|
||||
const msg = [
|
||||
'🔴 *Secretária IA — provedor sem saldo*',
|
||||
'',
|
||||
`O provedor *${prov}*${data.model ? ` (${data.model})` : ''} atingiu o limite de *cota/crédito*.`,
|
||||
'▫️ Status: cota/billing esgotado (429)',
|
||||
`▫️ Fallback em uso: ${fb}`,
|
||||
'',
|
||||
'⚠️ *Ação necessária:* recarregue o crédito da conta do provedor para manter o atendimento automático 24h.',
|
||||
'',
|
||||
`🕒 ${when}`,
|
||||
'_NewWhats · monitor de provedores de IA_',
|
||||
].join('\n');
|
||||
console.warn(`[secretaria] provider sem saldo: ${prov} (${data.model ?? '—'}) — fallback: ${fb}`);
|
||||
const secCfg = (await config.get('secretaria'));
|
||||
const adminPhone = typeof secCfg?.admin_notify_phone === 'string' ? secCfg.admin_notify_phone.trim() : '';
|
||||
if (!adminPhone)
|
||||
return;
|
||||
const instance = await prisma.instance.findFirst({ where: { tenantId: data.tenantId }, select: { id: true } });
|
||||
if (!instance)
|
||||
return;
|
||||
const sock = manager?.getSocket(instance.id);
|
||||
if (!sock)
|
||||
return;
|
||||
// Resolve o JID canônico via onWhatsApp — trata o 9º dígito (BR) e valida
|
||||
// se o número está no WhatsApp; sem isso, a concatenação crua pode não
|
||||
// corresponder ao JID real (mensagem "enviada" mas não entregue).
|
||||
const cand = (() => { const d = adminPhone.replace(/\D/g, ''); return d.startsWith('55') ? d : '55' + d; })();
|
||||
let jid = `${cand}@s.whatsapp.net`;
|
||||
try {
|
||||
const found = await sock.onWhatsApp(cand);
|
||||
if (found?.[0]?.exists && found[0]?.jid)
|
||||
jid = found[0].jid;
|
||||
else {
|
||||
console.warn(`[secretaria] admin ${cand} não está no WhatsApp — notificação não enviada`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch { /* fallback: usa o jid montado */ }
|
||||
await sock.sendMessage(jid, { text: msg });
|
||||
}
|
||||
catch { /* best-effort */ }
|
||||
});
|
||||
// ── GET /sessions ──────────────────────────────────────────────────────────
|
||||
// Lista todas as instâncias do tenant com status e telefone.
|
||||
router.get('/sessions', async (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user