mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
fix: crons do Pro entram no vercel.ts, nao num json ao lado
O deploy anterior caiu com 'Multiple configuration files found'. A lista completa fica no vercel.ts, que ja era o arquivo canonico.
This commit is contained in:
@@ -2,10 +2,11 @@
|
||||
|
||||
## Plano Pro
|
||||
|
||||
No Pro o relógio nativo é o `vercel.json` da raiz — a mesma cadência do
|
||||
No Pro o relógio nativo é o `vercel.ts` da raiz — a mesma cadência do
|
||||
`docker/scheduler/entrypoint.sh` (follow-up, dreno, dispatcher, agenda…).
|
||||
Expressões de minuto **quebram o deploy Hobby**. Este runbook é só para quem
|
||||
está no plano gratuito e precisa de um cron de fora.
|
||||
está no plano gratuito e precisa de um cron de fora. Não crie `vercel.json`
|
||||
ao lado do `vercel.ts`: a Vercel recusa os dois.
|
||||
|
||||
## Por que existe (Hobby)
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ const DIR_CRON = join(RAIZ, "app", "api", "v1", "cron");
|
||||
// do cron à internet da VPS. A cerca continua a mesma; só a fonte da verdade do
|
||||
// "o que roda" mudou de arquivo.
|
||||
const CRONTAB = join(RAIZ, "docker", "scheduler", "entrypoint.sh");
|
||||
const VERCEL_JSON = join(RAIZ, "vercel.json");
|
||||
const VERCEL_TS = join(RAIZ, "vercel.ts");
|
||||
|
||||
/** As rotas que existem, lidas do disco — não de uma lista mantida à mão. */
|
||||
function rotasNoCodigo(): string[] {
|
||||
@@ -78,24 +78,15 @@ describe("rotas de cron × agendamento no self-host", () => {
|
||||
});
|
||||
});
|
||||
|
||||
function rotasNoVercelJson(): string[] {
|
||||
const raw = JSON.parse(readFileSync(VERCEL_JSON, "utf8")) as {
|
||||
crons?: { path: string }[];
|
||||
};
|
||||
return [
|
||||
...new Set(
|
||||
(raw.crons ?? []).map((c) => {
|
||||
const nome = c.path.match(/\/api\/v1\/cron\/([a-z0-9-]+)/)?.[1];
|
||||
if (!nome) throw new Error(`cron path fora do contrato: ${c.path}`);
|
||||
return nome;
|
||||
}),
|
||||
),
|
||||
].sort();
|
||||
function rotasNoVercelTs(): string[] {
|
||||
const fonte = readFileSync(VERCEL_TS, "utf8");
|
||||
const achadas = fonte.matchAll(/path:\s*"\/api\/v1\/cron\/([a-z0-9-]+)"/g);
|
||||
return [...new Set([...achadas].map((m) => m[1]!))].sort();
|
||||
}
|
||||
|
||||
describe("rotas de cron × agendamento no Vercel Pro", () => {
|
||||
it("vercel.json agenda as mesmas rotas do scheduler", () => {
|
||||
expect(rotasNoVercelJson()).toEqual(rotasAgendadas());
|
||||
it("vercel.ts agenda as mesmas rotas do scheduler", () => {
|
||||
expect(rotasNoVercelTs()).toEqual(rotasAgendadas());
|
||||
});
|
||||
|
||||
it("documenta o CRON_SECRET da Vercel — senão o Pro agenda e a rota responde 403", () => {
|
||||
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
{
|
||||
"crons": [
|
||||
{
|
||||
"path": "/api/v1/cron/agent-dispatcher",
|
||||
"schedule": "* * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/followup-flow-worker",
|
||||
"schedule": "* * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/event-log-drain",
|
||||
"schedule": "* * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/routing-worker",
|
||||
"schedule": "* * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/recover-stuck-messages",
|
||||
"schedule": "* * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/storage-redaction",
|
||||
"schedule": "*/5 * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/snooze-watcher",
|
||||
"schedule": "*/5 * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/attendant-heartbeat",
|
||||
"schedule": "*/5 * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/webhook-log-retention",
|
||||
"schedule": "*/5 * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/channel-health",
|
||||
"schedule": "*/5 * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/agenda-google-push",
|
||||
"schedule": "*/5 * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/agenda-reminder",
|
||||
"schedule": "*/5 * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/contact-avatars",
|
||||
"schedule": "*/10 * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/agenda-google-refresh",
|
||||
"schedule": "*/10 * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/agenda-google-sync",
|
||||
"schedule": "*/15 * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/risk-watcher",
|
||||
"schedule": "*/15 * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/contact-phones",
|
||||
"schedule": "*/30 * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/contact-proposals-watcher",
|
||||
"schedule": "17 * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/lgpd-sla-watcher",
|
||||
"schedule": "0 12 * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/kb-conversations-batch",
|
||||
"schedule": "30 3 * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/sync-model-catalog",
|
||||
"schedule": "15 4 * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/cron/data-retention",
|
||||
"schedule": "40 4 * * *"
|
||||
}
|
||||
],
|
||||
"functions": {
|
||||
"app/api/v1/cron/**/route.ts": {
|
||||
"maxDuration": 120
|
||||
},
|
||||
"app/api/v1/system/relogio/tick/route.ts": {
|
||||
"maxDuration": 60
|
||||
},
|
||||
"app/api/internal/agents/run/route.ts": {
|
||||
"maxDuration": 300
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,45 @@
|
||||
/**
|
||||
* Vercel project config (canonical TS form).
|
||||
* Configuração do projeto na Vercel.
|
||||
*
|
||||
* Crons placeholder; lista final virá da Spec 08 (Operações & Workers).
|
||||
* Os 7 crons abaixo refletem os jobs derivados das specs herdadas:
|
||||
* - recover-stuck-messages (WAHA)
|
||||
* - sync-sessions (WAHA)
|
||||
* - process-pending-webhooks (event_log)
|
||||
* - dispatch-webhooks (deliveries / outbound webhooks)
|
||||
* - lgpd-data-request-worker (D+7 SLA)
|
||||
* - nuvemshop-sync-incremental
|
||||
* - audit-log-archive (cold storage)
|
||||
* Os crons abaixo são a MESMA cadência de `docker/scheduler/entrypoint.sh`.
|
||||
* Plano Pro: minuto a minuto vale. Hobby: expressões mais frequentes que
|
||||
* 1×/dia derrubam o deploy — nesse caso deixe só o `lgpd-sla-watcher` e use
|
||||
* o relógio HTTP (`docs/runbooks/vercel-hobby-relogio.md`).
|
||||
*
|
||||
* Auth de cron: header `Authorization: Bearer ${INTERNAL_SECRET}` validado em cada handler.
|
||||
* Auth: Vercel Cron manda Bearer CRON_SECRET; em produção `lib/env.ts` copia
|
||||
* isso para INTERNAL_CRON_SECRET. INTERNAL_SECRET continua valendo nas rotas.
|
||||
*/
|
||||
|
||||
import type { VercelConfig } from "@vercel/config/v1";
|
||||
|
||||
const config: VercelConfig = {
|
||||
// Plano Hobby só aceita cron ≤1×/dia. Os jobs */1 (agent-dispatcher,
|
||||
// routing-worker, event-log-drain) ficam de fora até Pro — no self-host
|
||||
// eles rodam no container `scheduler`. Reative-os ao subir de plano.
|
||||
crons: [
|
||||
{ path: "/api/v1/cron/agent-dispatcher", schedule: "* * * * *" },
|
||||
{ path: "/api/v1/cron/followup-flow-worker", schedule: "* * * * *" },
|
||||
{ path: "/api/v1/cron/event-log-drain", schedule: "* * * * *" },
|
||||
{ path: "/api/v1/cron/routing-worker", schedule: "* * * * *" },
|
||||
{ path: "/api/v1/cron/recover-stuck-messages", schedule: "* * * * *" },
|
||||
{ path: "/api/v1/cron/storage-redaction", schedule: "*/5 * * * *" },
|
||||
{ path: "/api/v1/cron/snooze-watcher", schedule: "*/5 * * * *" },
|
||||
{ path: "/api/v1/cron/attendant-heartbeat", schedule: "*/5 * * * *" },
|
||||
{ path: "/api/v1/cron/webhook-log-retention", schedule: "*/5 * * * *" },
|
||||
{ path: "/api/v1/cron/channel-health", schedule: "*/5 * * * *" },
|
||||
{ path: "/api/v1/cron/agenda-google-push", schedule: "*/5 * * * *" },
|
||||
{ path: "/api/v1/cron/agenda-reminder", schedule: "*/5 * * * *" },
|
||||
{ path: "/api/v1/cron/contact-avatars", schedule: "*/10 * * * *" },
|
||||
{ path: "/api/v1/cron/agenda-google-refresh", schedule: "*/10 * * * *" },
|
||||
{ path: "/api/v1/cron/agenda-google-sync", schedule: "*/15 * * * *" },
|
||||
{ path: "/api/v1/cron/risk-watcher", schedule: "*/15 * * * *" },
|
||||
{ path: "/api/v1/cron/contact-phones", schedule: "*/30 * * * *" },
|
||||
{ path: "/api/v1/cron/contact-proposals-watcher", schedule: "17 * * * *" },
|
||||
{ path: "/api/v1/cron/lgpd-sla-watcher", schedule: "0 12 * * *" },
|
||||
{ path: "/api/v1/cron/kb-conversations-batch", schedule: "30 3 * * *" },
|
||||
{ path: "/api/v1/cron/sync-model-catalog", schedule: "15 4 * * *" },
|
||||
{ path: "/api/v1/cron/data-retention", schedule: "40 4 * * *" },
|
||||
],
|
||||
functions: {
|
||||
// EPIC-13 S-13.08: ToolLoopAgent runtime can issue multiple tool calls per
|
||||
// step. 300s max keeps Fluid Compute within bounds; the runtime's own
|
||||
// step/token/cost guards usually finish much earlier.
|
||||
"app/api/v1/cron/**/route.ts": { maxDuration: 120 },
|
||||
"app/api/v1/system/relogio/tick/route.ts": { maxDuration: 60 },
|
||||
"app/api/internal/agents/run/route.ts": { maxDuration: 300 },
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user