Files
clube67_newwhats.local/newwhats.clube67.com/newwhats.local/plugins/secretaria/routes.js
T
VPS 4 Deploy Agent 213990d51f feat(secretaria): Secretária IA separada por sessão (agente por número)
Núcleo desta mudança (separação por sessão):
- migrate.ts: sec_numbers.agent_id (FK→sec_agents, ON DELETE SET NULL) e
  sec_calendar.instance_id, com índices (aditivo/idempotente).
- ext-api routes.ts: runtime resolve o agente pelo NÚMERO que recebeu a
  mensagem (sec_numbers[instanceId].agent_id, fallback: 1º ativo); rotas
  admin /secretaria/numbers e /secretaria/calendar aceitam os novos campos.
- brain.ts/tools.ts: instanceId no contexto; sec_calendar interno filtrado
  por sessão; parse de jid robusto (.pop(), suporta 2 e 3 partes).
- secretaria/routes.ts: paridade (agent_id em numbers, instance_id no calendar).

Testado em dev (newwhats.dev refrescado): agentes resolvidos por instância;
fallback e FK ON DELETE SET NULL verificados via ext-api.

Obs.: por estarem no mesmo working tree não-commitado do newwhats.local,
ext-api/routes.ts e secretaria/tools.ts também trazem o trabalho acumulado
da integração do satélite (ações de inbox: delete/react/typing/labels/forward
e admin /secretaria/*; tools identificar_numero/cadastrar_paciente do odonto).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 19:51:25 +02:00

312 lines
13 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createSecretariaRoutes = createSecretariaRoutes;
const express_1 = require("express");
const brain_1 = require("./brain");
function uuid() {
try {
return crypto.randomUUID();
}
catch {
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
}
function createSecretariaRoutes(db, config) {
const router = (0, express_1.Router)();
const brain = new brain_1.ProtocolEngine(db, config);
// ── Agents ───────────────────────────────────────────────────────────────
router.get('/agents', async (_req, res) => {
try {
const agents = await db('sec_agents').orderBy('created_at');
res.json(agents);
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
router.post('/agents', async (req, res) => {
try {
const { name, description, model, provider, temperature, context_window } = req.body;
const [agent] = await db('sec_agents').insert({
id: uuid(), name, description, model: model ?? 'gpt-4o-mini',
provider: provider ?? 'openai', temperature: temperature ?? 0.7,
context_window: context_window ?? 8, active: true,
}).returning('*');
res.status(201).json(agent);
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
router.put('/agents/:id', async (req, res) => {
try {
const { id } = req.params;
const { name, description, model, provider, temperature, context_window, active } = req.body;
const [agent] = await db('sec_agents').where({ id })
.update({ name, description, model, provider, temperature, context_window, active, updated_at: new Date() })
.returning('*');
res.json(agent);
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
router.delete('/agents/:id', async (req, res) => {
try {
await db('sec_agents').where({ id: req.params.id }).delete();
res.json({ ok: true });
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
// ── Brain Nodes ───────────────────────────────────────────────────────────
router.get('/agents/:agentId/nodes', async (req, res) => {
try {
const nodes = await db('sec_brain_nodes')
.where({ agent_id: req.params.agentId })
.orderBy('sort_order');
res.json(nodes);
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
router.post('/agents/:agentId/nodes', async (req, res) => {
try {
const { type, title, content, sort_order } = req.body;
const [node] = await db('sec_brain_nodes').insert({
id: uuid(), agent_id: req.params.agentId,
type, title, content, active: true,
sort_order: sort_order ?? 99,
}).returning('*');
res.status(201).json(node);
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
router.put('/nodes/:id', async (req, res) => {
try {
const { title, content, active, sort_order } = req.body;
const [node] = await db('sec_brain_nodes').where({ id: req.params.id })
.update({ title, content, active, sort_order, updated_at: new Date() })
.returning('*');
res.json(node);
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
router.delete('/nodes/:id', async (req, res) => {
try {
await db('sec_brain_nodes').where({ id: req.params.id }).delete();
res.json({ ok: true });
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
// ── Conversations ────────────────────────────────────────────────────────
router.get('/conversations', async (req, res) => {
try {
const { agent_id } = req.query;
let q = db('sec_conversations').orderBy('updated_at', 'desc');
if (agent_id)
q = q.where({ agent_id: agent_id });
const convs = await q;
res.json(convs);
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
router.post('/conversations', async (req, res) => {
try {
const { agent_id, contact_name } = req.body;
const [conv] = await db('sec_conversations').insert({
id: uuid(), agent_id,
contact_name: contact_name ?? 'Usuário Teste',
protocol_number: brain_1.ProtocolEngine.generateProtocolNumber(),
status: 'active',
}).returning('*');
res.status(201).json(conv);
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
router.patch('/conversations/:id', async (req, res) => {
try {
const { status, contact_name } = req.body;
const [conv] = await db('sec_conversations').where({ id: req.params.id })
.update({ status, contact_name, updated_at: new Date() }).returning('*');
res.json(conv);
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
router.delete('/conversations/:id', async (req, res) => {
try {
await db('sec_conversations').where({ id: req.params.id }).delete();
res.json({ ok: true });
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
// ── Messages ──────────────────────────────────────────────────────────────
router.get('/conversations/:id/messages', async (req, res) => {
try {
const messages = await db('sec_messages')
.where({ conversation_id: req.params.id })
.orderBy('created_at');
res.json(messages);
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
// Enviar mensagem → aciona o cérebro → retorna resposta da IA
router.post('/conversations/:id/chat', async (req, res) => {
try {
const { message } = req.body;
if (!message?.trim())
return res.status(400).json({ error: 'message is required' });
const reply = await brain.chat(String(req.params.id), message.trim());
res.json({ reply });
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
// Finalizar protocolo → gera resumo completo → apaga mensagens → fecha conversa
router.post('/conversations/:id/finalize', async (req, res) => {
try {
const result = await brain.finalizeProtocol(String(req.params.id));
res.json(result);
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
// ── Calendar ──────────────────────────────────────────────────────────────
router.get('/calendar', async (req, res) => {
try {
const qs = (k) => { const v = req.query[k]; return v ? String(v) : undefined; };
const from = qs('from'), to = qs('to'), status = qs('status'), instanceId = qs('instance_id');
let q = db('sec_calendar').orderBy('date').orderBy('time_start');
if (from)
q = q.where('date', '>=', from);
if (to)
q = q.where('date', '<=', to);
if (status)
q = q.where({ status });
// Escopo por sessão: slots do número + globais (instance_id NULL).
if (instanceId)
q = q.where((qb) => qb.whereNull('instance_id').orWhere('instance_id', instanceId));
res.json(await q);
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
router.post('/calendar', async (req, res) => {
try {
const { title, date, time_start, time_end, attendee_name, attendee_phone, notes, instance_id } = req.body;
const [slot] = await db('sec_calendar').insert({
id: uuid(), title, date, time_start, time_end, instance_id: instance_id ?? null,
attendee_name, attendee_phone, notes, status: 'available',
}).returning('*');
res.status(201).json(slot);
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
router.put('/calendar/:id', async (req, res) => {
try {
const { title, date, time_start, time_end, attendee_name, attendee_phone, status, notes, instance_id } = req.body;
const patch = { title, date, time_start, time_end, attendee_name, attendee_phone, status, notes, updated_at: new Date() };
if ('instance_id' in req.body)
patch.instance_id = instance_id ?? null;
const [slot] = await db('sec_calendar').where({ id: req.params.id })
.update(patch)
.returning('*');
res.json(slot);
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
router.delete('/calendar/:id', async (req, res) => {
try {
await db('sec_calendar').where({ id: req.params.id }).delete();
res.json({ ok: true });
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
// ── Numbers ───────────────────────────────────────────────────────────────
router.get('/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 });
}
});
router.post('/numbers', async (req, res) => {
try {
const { instance_id, label, role, area, priority, notes, agent_id } = req.body;
if (!instance_id)
return res.status(400).json({ error: 'instance_id é obrigatório' });
// Upsert: se já existe um registro para esse instance_id, atualiza o role
const existing = await db('sec_numbers').where({ instance_id }).first();
if (existing) {
const patch = { label, role: role ?? 'clinic', area: area ?? null, priority: priority ?? 10, notes: notes ?? null, updated_at: new Date() };
if ('agent_id' in req.body)
patch.agent_id = agent_id ?? null;
const [num] = await db('sec_numbers').where({ instance_id }).update(patch).returning('*');
return res.json(num);
}
const [num] = await db('sec_numbers').insert({
id: uuid(), instance_id, label, role: role ?? 'clinic', agent_id: agent_id ?? null,
area: area ?? null, priority: priority ?? 10, active: true, notes: notes ?? null,
}).returning('*');
res.status(201).json(num);
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
router.put('/numbers/:id', async (req, res) => {
try {
const { label, role, area, instance_id, priority, active, notes, agent_id } = req.body;
const patch = { label, role, area, instance_id, priority, active, notes, updated_at: new Date() };
if ('agent_id' in req.body)
patch.agent_id = agent_id ?? null;
const [num] = await db('sec_numbers').where({ id: req.params.id })
.update(patch)
.returning('*');
res.json(num);
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
router.delete('/numbers/:id', async (req, res) => {
try {
await db('sec_numbers').where({ id: req.params.id }).delete();
res.json({ ok: true });
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
return router;
}
//# sourceMappingURL=routes.js.map