feat(ext-api): API REST /secretaria/* (agents/nodes/calendar/numbers/conversations)
Expõe o contrato de administração esperado pelo painel da Secretária no satélite (antes só havia /secretaria/ask|numbers + /sec/* parcial → 404 em agents/calendar): - agents: GET/POST/PUT/DELETE - brain nodes: GET/POST por agente (/agents/:agentId/nodes) + PUT/DELETE (/nodes/:id) - calendar: GET/POST/PUT/DELETE - numbers: POST/PUT/DELETE (GET já existia) - conversations (teste): list/create/patch/delete, /messages, /chat (brain.chat), /finalize (brain.finalizeProtocol) Backed pelas tabelas sec_* (globais/single-tenant no motor). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -935,6 +935,184 @@ export function buildExtRoutes(
|
||||
}
|
||||
})
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Secretária — API REST de administração (/secretaria/*) do painel do satélite.
|
||||
// As tabelas sec_* são GLOBAIS no motor (single-tenant); estes endpoints
|
||||
// expõem o contrato completo (agents/nodes/calendar/numbers/conversations)
|
||||
// esperado pelo frontend, reaproveitando a lógica dos /sec/*.
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// ── Agents ──────────────────────────────────────────────────────────────────
|
||||
router.get('/secretaria/agents', async (_req: Request, res: Response) => {
|
||||
try { res.json(await db('sec_agents').orderBy('created_at')) }
|
||||
catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
router.post('/secretaria/agents', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const b = req.body ?? {}
|
||||
const [a] = await db('sec_agents').insert({
|
||||
id: uuid(), name: b.name ?? 'Agente', description: b.description ?? null,
|
||||
model: b.model ?? 'gemini-2.0-flash', provider: b.provider ?? 'gemini',
|
||||
temperature: b.temperature ?? 0.7, context_window: b.context_window ?? 10, active: true,
|
||||
}).returning('*')
|
||||
res.status(201).json(a)
|
||||
} catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
router.put('/secretaria/agents/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const b = req.body ?? {}, patch: any = { updated_at: new Date() }
|
||||
for (const k of ['name', 'description', 'model', 'provider', 'temperature', 'context_window', 'active']) if (k in b) patch[k] = b[k]
|
||||
const [a] = await db('sec_agents').where({ id: req.params['id'] }).update(patch).returning('*')
|
||||
res.json(a)
|
||||
} catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
router.delete('/secretaria/agents/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const id = req.params['id']
|
||||
await db('sec_brain_nodes').where({ agent_id: id }).delete()
|
||||
await db('sec_agents').where({ id }).delete()
|
||||
res.json({ ok: true })
|
||||
} catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
|
||||
// ── Brain nodes (por agente) ──────────────────────────────────────────────
|
||||
router.get('/secretaria/agents/:agentId/nodes', async (req: Request, res: Response) => {
|
||||
try { res.json(await db('sec_brain_nodes').where({ agent_id: req.params['agentId'] }).orderBy('sort_order')) }
|
||||
catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
router.post('/secretaria/agents/:agentId/nodes', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const b = req.body ?? {}
|
||||
const [n] = await db('sec_brain_nodes').insert({
|
||||
id: uuid(), agent_id: req.params['agentId'], type: b.type, title: b.title ?? '', content: b.content ?? '',
|
||||
node_model: b.node_model ?? null, active: true, sort_order: b.sort_order ?? 99,
|
||||
}).returning('*')
|
||||
res.status(201).json(n)
|
||||
} catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
router.put('/secretaria/nodes/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const b = req.body ?? {}, patch: any = { updated_at: new Date() }
|
||||
for (const k of ['type', 'title', 'content', 'node_model', 'active', 'sort_order']) if (k in b) patch[k] = b[k]
|
||||
const [n] = await db('sec_brain_nodes').where({ id: req.params['id'] }).update(patch).returning('*')
|
||||
res.json(n)
|
||||
} catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
router.delete('/secretaria/nodes/:id', async (req: Request, res: Response) => {
|
||||
try { await db('sec_brain_nodes').where({ id: req.params['id'] }).delete(); res.json({ ok: true }) }
|
||||
catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
|
||||
// ── Calendar ──────────────────────────────────────────────────────────────
|
||||
router.get('/secretaria/calendar', async (_req: Request, res: Response) => {
|
||||
try { res.json(await db('sec_calendar').orderBy('date').orderBy('time_start')) }
|
||||
catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
router.post('/secretaria/calendar', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const b = req.body ?? {}
|
||||
const [s] = await db('sec_calendar').insert({
|
||||
id: uuid(), title: b.title ?? null, date: b.date, time_start: b.time_start, time_end: b.time_end,
|
||||
attendee_name: b.attendee_name ?? null, attendee_phone: b.attendee_phone ?? null,
|
||||
status: b.status ?? 'available', notes: b.notes ?? null,
|
||||
}).returning('*')
|
||||
res.status(201).json(s)
|
||||
} catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
router.put('/secretaria/calendar/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const b = req.body ?? {}, patch: any = { updated_at: new Date() }
|
||||
for (const k of ['title', 'date', 'time_start', 'time_end', 'attendee_name', 'attendee_phone', 'status', 'notes']) if (k in b) patch[k] = b[k]
|
||||
const [s] = await db('sec_calendar').where({ id: req.params['id'] }).update(patch).returning('*')
|
||||
res.json(s)
|
||||
} catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
router.delete('/secretaria/calendar/:id', async (req: Request, res: Response) => {
|
||||
try { await db('sec_calendar').where({ id: req.params['id'] }).delete(); res.json({ ok: true }) }
|
||||
catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
|
||||
// ── Numbers (GET acima) — mutações ─────────────────────────────────────────
|
||||
router.post('/secretaria/numbers', async (req: Request, res: Response) => {
|
||||
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,
|
||||
}).returning('*')
|
||||
res.status(201).json(n)
|
||||
} catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
router.put('/secretaria/numbers/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const b = req.body ?? {}, patch: any = { updated_at: new Date() }
|
||||
for (const k of ['instance_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('*')
|
||||
res.json(n)
|
||||
} catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
router.delete('/secretaria/numbers/:id', async (req: Request, res: Response) => {
|
||||
try { await db('sec_numbers').where({ id: req.params['id'] }).delete(); res.json({ ok: true }) }
|
||||
catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
|
||||
// ── Conversations (teste do painel) ────────────────────────────────────────
|
||||
router.get('/secretaria/conversations', async (req: Request, res: Response) => {
|
||||
try {
|
||||
let q = db('sec_conversations')
|
||||
.select('id', 'agent_id', 'protocol_number', 'contact_name', 'status', 'handoff_mode', 'summary', 'created_at', 'updated_at')
|
||||
.orderBy('updated_at', 'desc').limit(100)
|
||||
const agentId = req.query['agent_id'] as string | undefined
|
||||
if (agentId) q = q.where({ agent_id: agentId })
|
||||
res.json(await q)
|
||||
} catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
router.post('/secretaria/conversations', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const b = req.body ?? {}
|
||||
const [c] = await db('sec_conversations').insert({
|
||||
id: uuid(), agent_id: b.agent_id, contact_name: b.contact_name ?? 'Teste',
|
||||
protocol_number: ProtocolEngine.generateProtocolNumber(), status: 'active', handoff_mode: 'ia',
|
||||
}).returning('*')
|
||||
res.status(201).json(c)
|
||||
} catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
router.patch('/secretaria/conversations/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const b = req.body ?? {}, patch: any = { updated_at: new Date() }
|
||||
for (const k of ['status', 'contact_name', 'handoff_mode', 'summary']) if (k in b) patch[k] = b[k]
|
||||
const [c] = await db('sec_conversations').where({ id: req.params['id'] }).update(patch).returning('*')
|
||||
res.json(c)
|
||||
} catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
router.delete('/secretaria/conversations/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const id = req.params['id']
|
||||
await db('sec_messages').where({ conversation_id: id }).delete()
|
||||
await db('sec_conversations').where({ id }).delete()
|
||||
res.json({ ok: true })
|
||||
} catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
router.get('/secretaria/conversations/:id/messages', async (req: Request, res: Response) => {
|
||||
try { res.json(await db('sec_messages').where({ conversation_id: req.params['id'] }).orderBy('created_at')) }
|
||||
catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
router.post('/secretaria/conversations/:id/chat', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const message = String((req.body ?? {}).message ?? '').trim()
|
||||
if (!message) { res.status(400).json({ error: 'message obrigatório' }); return }
|
||||
const brain = new ProtocolEngine(db, config)
|
||||
const reply = await brain.chat(req.params['id']!, message, { tenantId: req.extTenantId, hooks })
|
||||
res.json({ reply })
|
||||
} catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
router.post('/secretaria/conversations/:id/finalize', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const brain = new ProtocolEngine(db, config)
|
||||
res.json(await brain.finalizeProtocol(req.params['id']!))
|
||||
} catch (err: any) { res.status(500).json({ error: err.message }) }
|
||||
})
|
||||
|
||||
// ── GET /sec/handoff/:chatId ──────────────────────────────────────────────
|
||||
// Retorna o modo de handoff atual para um chat específico.
|
||||
router.get('/sec/handoff/:chatId', async (req: Request, res: Response) => {
|
||||
|
||||
Reference in New Issue
Block a user