GabrielRamison 2 月之前
父節點
當前提交
6e66fbc819

+ 1 - 0
.env.example

@@ -81,3 +81,4 @@ RAG_HYDE=false
 IFBOT_BASE_URL=https://star.ifbot.com.br/api
 IFBOT_TOKEN=
 # IFBOT_AUTH_HEADER=Authorization
+ATENDIMENTOS_SYNC_INTERVAL_MINUTES=30

+ 2 - 1
src/config/index.js

@@ -72,7 +72,8 @@ export const config = {
   ifbot: {
     baseUrl: process.env.IFBOT_BASE_URL ?? "https://star.ifbot.com.br/api",
     token: process.env.IFBOT_TOKEN ?? "",
-    authHeader: process.env.IFBOT_AUTH_HEADER ?? "Authorization"
+    authHeader: process.env.IFBOT_AUTH_HEADER ?? "Authorization",
+    syncIntervalMinutes: Number(process.env.ATENDIMENTOS_SYNC_INTERVAL_MINUTES ?? 30)
   }
 };
 

+ 10 - 0
src/controllers/Atendimentos.Controller.js

@@ -3,6 +3,7 @@ import {
   fetchAtendimentoJson,
   salvarAtendimento,
   sincronizarProtocolosAbertos,
+  sincronizarTodosProtocolosAbertos,
   listarAtendimentos,
   obterAtendimento
 } from "../services/atendimentosService.js";
@@ -38,6 +39,15 @@ export const AtendimentosController = {
     }
   },
 
+  SincronizarTudo: async function (req, res, next) {
+    try {
+      const result = await sincronizarTodosProtocolosAbertos(req.body);
+      res.json({ ok: true, ...result });
+    } catch (err) {
+      next(err);
+    }
+  },
+
   Listar: async function (req, res, next) {
     try {
       const page = Math.max(1, Number(req.query.page) || 1);

+ 2 - 0
src/factories/Server.factory.js

@@ -9,6 +9,7 @@ import { qdrant } from "../services/qdrantClient.js";
 import { errorHandler } from "../middleware/ErrorHandler.js";
 import { Roteamento } from "../routes/index.js";
 import { scheduleCleanup } from "../jobs/cleanupTokens.js";
+import { scheduleSyncAtendimentos } from "../jobs/syncAtendimentos.js";
 
 export class ServerFactory {
   static Iniciar() {
@@ -53,6 +54,7 @@ export class ServerFactory {
     const server = app.listen(port, () => {
       process.stdout.write(`API listening on http://localhost:${port}\n`);
       scheduleCleanup();
+      scheduleSyncAtendimentos();
     });
 
     this.app = app;

+ 29 - 0
src/jobs/syncAtendimentos.js

@@ -0,0 +1,29 @@
+import { config } from "../config/index.js";
+import { sincronizarTodosProtocolosAbertos } from "../services/atendimentosService.js";
+
+export async function runSyncAtendimentos() {
+  const r = await sincronizarTodosProtocolosAbertos();
+  console.log(
+    `[sync-atendimentos] ${r.importados}/${r.processados} protocolos importados, ` +
+    `${r.mensagens} mensagens, ${r.erros.length} erros (${r.paginas} páginas)`
+  );
+  return r;
+}
+
+export function scheduleSyncAtendimentos(intervalMs = config.ifbot.syncIntervalMinutes * 60_000) {
+  if (!config.ifbot.token) {
+    console.warn("[sync-atendimentos] desabilitado — IFBOT_TOKEN não configurado");
+    return null;
+  }
+  if (!intervalMs || intervalMs <= 0) {
+    console.warn("[sync-atendimentos] desabilitado — ATENDIMENTOS_SYNC_INTERVAL_MINUTES=0");
+    return null;
+  }
+
+  console.log(`[sync-atendimentos] agendado a cada ${Math.round(intervalMs / 60_000)} min`);
+  // primeira execução só após um intervalo, para não martelar a API do ifbot
+  // a cada restart do dev server (--watch)
+  return setInterval(() => {
+    runSyncAtendimentos().catch((e) => console.error("[sync-atendimentos] falha:", e.message));
+  }, intervalMs).unref();
+}

+ 5 - 0
src/middleware/schemas/Atendimento.Schema.js

@@ -73,3 +73,8 @@ export const atendimentoSyncSchema = z.object({
   inicio: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
   fim: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional()
 });
+
+export const atendimentoSyncAllSchema = z.object({
+  limite: z.number().int().positive().max(200).optional(),
+  maxPaginas: z.number().int().positive().max(100).optional()
+});

+ 2 - 1
src/routes/Atendimentos.Rotas.js

@@ -4,12 +4,13 @@ import { requireUser } from "../middleware/RequireUser.js";
 import { requireAdmin } from "../middleware/RequireAdmin.js";
 import { parseId } from "../middleware/ParseId.js";
 import { validate } from "../middleware/Validate.js";
-import { atendimentoBodySchema, atendimentoUrlSchema, atendimentoSyncSchema } from "../middleware/schemas/Atendimento.Schema.js";
+import { atendimentoBodySchema, atendimentoUrlSchema, atendimentoSyncSchema, atendimentoSyncAllSchema } from "../middleware/schemas/Atendimento.Schema.js";
 
 export const atendimentosRouter = Router();
 
 atendimentosRouter.post("/", requireUser, requireAdmin, validate({ body: atendimentoBodySchema }), AtendimentosController.Ingerir);
 atendimentosRouter.post("/url", requireUser, requireAdmin, validate({ body: atendimentoUrlSchema }), AtendimentosController.IngerirUrl);
 atendimentosRouter.post("/sync", requireUser, requireAdmin, validate({ body: atendimentoSyncSchema }), AtendimentosController.Sincronizar);
+atendimentosRouter.post("/sync-all", requireUser, requireAdmin, validate({ body: atendimentoSyncAllSchema }), AtendimentosController.SincronizarTudo);
 atendimentosRouter.get("/", requireUser, AtendimentosController.Listar);
 atendimentosRouter.get("/:id", requireUser, parseId, AtendimentosController.Obter);

+ 31 - 0
src/services/atendimentosService.js

@@ -238,6 +238,37 @@ export async function sincronizarProtocolosAbertos({ pagina = 1, limite = 50, in
   };
 }
 
+let fullSyncEmAndamento = false;
+
+export async function sincronizarTodosProtocolosAbertos({ limite = 200, maxPaginas = 50 } = {}) {
+  if (fullSyncEmAndamento) {
+    const err = new Error("sync_em_andamento");
+    err.statusCode = 409;
+    throw err;
+  }
+  fullSyncEmAndamento = true;
+
+  try {
+    const agregado = { total: 0, paginas: 0, processados: 0, importados: 0, mensagens: 0, erros: [] };
+
+    for (let pagina = 1; pagina <= maxPaginas; pagina += 1) {
+      const r = await sincronizarProtocolosAbertos({ pagina, limite });
+      agregado.total = r.total;
+      agregado.paginas = pagina;
+      agregado.processados += r.processados;
+      agregado.importados += r.importados;
+      agregado.mensagens += r.mensagens;
+      agregado.erros.push(...r.erros);
+
+      if (r.processados < limite || agregado.processados >= agregado.total) break;
+    }
+
+    return agregado;
+  } finally {
+    fullSyncEmAndamento = false;
+  }
+}
+
 export async function listarAtendimentos({ page = 1, pageSize = 20, setor, status } = {}) {
   const query = Atendimento.query()
     .withGraphFetched("cliente")