hobots 3.4.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -28
- package/dist/{chunk-5WIIACWC.js → chunk-HKCDMUA3.js} +1 -2
- package/dist/chunk-HKCDMUA3.js.map +1 -0
- package/dist/index.cjs +179 -231
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -25
- package/dist/index.d.ts +47 -25
- package/dist/index.js +180 -231
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs +0 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +21 -23
- package/dist/protocol.d.ts +21 -23
- package/dist/protocol.js +1 -1
- package/docs/README.md +10 -8
- package/docs/api-reference.md +34 -20
- package/docs/concepts.md +19 -19
- package/docs/getting-started.md +14 -12
- package/docs/protocol.md +24 -25
- package/docs/{tasks.md → runs.md} +51 -23
- package/package.json +2 -2
- package/dist/chunk-5WIIACWC.js.map +0 -1
package/docs/concepts.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Conceitos
|
|
2
2
|
|
|
3
|
-
Esta página explica **o que cada coisa é** — o vocabulário que as outras páginas usam:
|
|
3
|
+
Esta página explica **o que cada coisa é** — o vocabulário que as outras páginas usam: agente, execução, item, evento, issue, transaction, step, heartbeat, instância, claim. As outras páginas mostram **como usar**; aqui está o **por que existe**.
|
|
4
4
|
|
|
5
5
|
Defaults numéricos vivem em [Tempos e intervalos](./api-reference.md#tempos-e-intervalos) e nas [tabelas de opções](./api-reference.md#tabelas-de-opções).
|
|
6
6
|
|
|
@@ -8,14 +8,14 @@ Defaults numéricos vivem em [Tempos e intervalos](./api-reference.md#tempos-e-i
|
|
|
8
8
|
|
|
9
9
|
| Conceito | Em uma frase | Quando você usa |
|
|
10
10
|
| --- | --- | --- |
|
|
11
|
-
| [
|
|
11
|
+
| [Agente](#agente) | O seu robô: um processo, uma coisa que ele faz | `hobots.register` / `hobots.start` |
|
|
12
12
|
| [Modo sob demanda](#modo-sob-demanda-register) | Uma execução por solicitação do app | O trabalho é disparado por alguém (formulário, agendamento) |
|
|
13
13
|
| [Modo sempre ligado](#modo-sempre-ligado-start) | O robô abre a própria execução e roda o seu loop | O robô vive rodando e você só quer ver o que ele faz |
|
|
14
|
-
| [Execução (run)](#execução-run) | Uma rodada
|
|
14
|
+
| [Execução (run)](#execução-run) | Uma rodada do agente, com logs, itens e resultado | A unidade de tudo em Trabalhos |
|
|
15
15
|
| [Item](#item) | Uma unidade de trabalho entregue | Cada nota, boleto, arquivo processado |
|
|
16
16
|
| [Progresso](#progresso) | Quantos de quantos | Barra e tempo restante no app |
|
|
17
17
|
| [Log da execução](#log-da-execução) | Narração do que a execução fez | `hobots.log` — nunca lança |
|
|
18
|
-
| [Anexo](#anexo) | Um print pendurado
|
|
18
|
+
| [Anexo](#anexo) | Um print pendurado num log, item ou step | "Como estava a tela?" |
|
|
19
19
|
| [Cancelamento cooperativo](#cancelamento-cooperativo) | O app pede, o handler para no próximo ponto de parada | Automático em items/progress/steps/sleep/retry |
|
|
20
20
|
| [Retry](#retry) | Tentar de novo na mesma execução | Instabilidade passageira |
|
|
21
21
|
| [Evento e issue](#evento-e-issue) | A foto de um erro, agrupada em issue | `hobots.captureException` e erros não tratados |
|
|
@@ -31,39 +31,39 @@ Defaults numéricos vivem em [Tempos e intervalos](./api-reference.md#tempos-e-i
|
|
|
31
31
|
|
|
32
32
|
| No SDK | No app |
|
|
33
33
|
| --- | --- |
|
|
34
|
-
| **Agente** — dono do Client Secret (`hb_…`) que autentica tudo | **Configurações → Agentes** |
|
|
35
|
-
| **Instância** — o par `(agente, instanceId)`; heartbeat e
|
|
36
|
-
| **
|
|
34
|
+
| **Agente** — dono do Client Secret (`hb_…`) que autentica tudo; o que ele faz é o handler de `register()`/`start()` | **Configurações → Agentes** (o formulário do agente fica aqui) |
|
|
35
|
+
| **Instância** — o par `(agente, instanceId)`; heartbeat e execuções convergem nela, com o modo (sob demanda / sempre ligado) | Módulo **Instâncias** (uptime, recursos da máquina, performance) |
|
|
36
|
+
| **Execução** — cada solicitação atendida por `register()`, ou a execução que `start()` abre sozinho | Módulo **Trabalhos** |
|
|
37
37
|
| **Evento** agrupado em **issue** — `captureException` e erros não tratados | Módulo **Issues** |
|
|
38
38
|
| **Transaction / steps** — `hobots.transaction` | Aba de performance da execução |
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
-
##
|
|
42
|
+
## Agente
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
O **agente** é o seu robô: um programa que faz **uma coisa** — "processar NF-e", "monitorar a pasta de entrada". Na plataforma ele é identificado pelo Client Secret, não por um nome no código: o processo que sobe com aquele secret *é* o agente, e o handler que você passa a `register()`/`start()` *é* a tarefa. Essa chamada é a única forma de configurar o SDK: não há `init()`. Ela lê as credenciais do ambiente, liga o heartbeat e a captura de erros, define as opções (`environment`, `release`, `tags`, `items`…) e diz como o robô trabalha. **Uma chamada por processo** — a segunda lança. Um robô que faz duas coisas são dois agentes, cada um com o seu processo e o seu Client Secret.
|
|
45
45
|
|
|
46
46
|
## Modo sob demanda (`register`)
|
|
47
47
|
|
|
48
|
-
O robô fica **online aguardando**: faz polling na plataforma e, quando alguém cria uma solicitação para
|
|
48
|
+
O robô fica **online aguardando**: faz polling na plataforma e, quando alguém cria uma solicitação para o agente (pelo formulário do agente ou por um agendamento), executa o handler com os `params` do formulário. É o modo para trabalho que **alguém pede**. Várias solicitações podem rodar em paralelo (`concurrency`). No painel a instância aparece como "sob demanda".
|
|
49
49
|
|
|
50
50
|
## Modo sempre ligado (`start`)
|
|
51
51
|
|
|
52
|
-
O robô **não espera ninguém**: ao iniciar, abre a própria execução na plataforma (`source: agent`, sem formulário nem solicitante) e roda o handler dentro dela. O handler é o seu loop — um `while (true)` que vigia uma pasta, uma fila, um portal. A plataforma recebe logs, itens e progresso enquanto ele roda. Quando o handler retorna, a execução fecha; o processo continua vivo pelo heartbeat, então a instância segue online até `hobots.close()`. É **uma execução por processo**: para recomeçar, reinicie o robô. Como o servidor guarda no máximo 1000 linhas de log por execução, o trabalho feito vai em itens, não em log.
|
|
52
|
+
O robô **não espera ninguém**: ao iniciar, abre a própria execução na plataforma (`source: agent`, sem formulário nem solicitante) e roda o handler dentro dela. O handler é o seu loop — um `while (true)` que vigia uma pasta, uma fila, um portal. A plataforma recebe logs, itens e progresso enquanto ele roda. Quando o handler retorna, a execução fecha; o processo continua vivo pelo heartbeat, então a instância segue online até `hobots.close()`. É **uma execução por processo**: para recomeçar, reinicie o robô. Como o servidor guarda no máximo 1000 linhas de log por execução, o trabalho feito vai em itens, não em log. No painel a instância aparece como "sempre ligado".
|
|
53
53
|
|
|
54
54
|
## Execução (run)
|
|
55
55
|
|
|
56
|
-
Uma **execução** é uma rodada concreta
|
|
56
|
+
Uma **execução** é uma rodada concreta do agente — no app, um trabalho. Ciclo de vida no servidor:
|
|
57
57
|
|
|
58
58
|
```
|
|
59
59
|
pending → claimed → running → succeeded | failed | canceled
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
No modo sob demanda a execução nasce `pending` (alguém pediu), vira `claimed` quando um agente a [reivindica](#claim) e `running` quando o handler começa. No modo sempre ligado ela já nasce `running`, criada pelo próprio robô. O agente só transiciona para `running`, `succeeded` e `failed`; `canceled` vem de alguém cancelando no app. Cada execução tem a sua transaction de performance, seus logs, itens e progresso — e o objeto `hobots` aponta para a execução **atual**, em qualquer módulo, graças ao contexto async do Node.
|
|
62
|
+
No modo sob demanda a execução nasce `pending` (alguém pediu), vira `claimed` quando um agente a [reivindica](#claim) e `running` quando o handler começa. No modo sempre ligado ela já nasce `running`, criada pelo próprio robô. O agente só transiciona para `running`, `succeeded` e `failed`; `canceled` vem de alguém cancelando no app. Cada execução tem a sua transaction de performance (`run`), seus logs, itens e progresso — e o objeto `hobots` aponta para a execução **atual**, em qualquer módulo, graças ao contexto async do Node.
|
|
63
63
|
|
|
64
64
|
## Item
|
|
65
65
|
|
|
66
|
-
Um **item** é uma unidade de trabalho que a execução entregou: uma nota baixada, um boleto emitido, um arquivo movido. Tem status (`succeeded`, `failed` = falha técnica, `occurrence` = ocorrência de negócio, como "nota não encontrada") e um `payload` de atributos livres que viram colunas na aba de itens da
|
|
66
|
+
Um **item** é uma unidade de trabalho que a execução entregou: uma nota baixada, um boleto emitido, um arquivo movido. Tem status (`succeeded`, `failed` = falha técnica, `occurrence` = ocorrência de negócio, como "nota não encontrada") e um `payload` de atributos livres que viram colunas na aba de itens da execução — as colunas são declaradas no código, uma vez, pela opção `items` do agente. O `id` é a identidade de negócio do item na execução — repetido, o servidor atualiza em vez de duplicar: é isso que torna retries idempotentes. Diferente do log, o item é **dado estruturado**: dá para contar, filtrar e conferir.
|
|
67
67
|
|
|
68
68
|
## Progresso
|
|
69
69
|
|
|
@@ -71,11 +71,11 @@ O **progresso** é "quantos de quantos" (`current/total`). Declare o total antes
|
|
|
71
71
|
|
|
72
72
|
## Log da execução
|
|
73
73
|
|
|
74
|
-
O **log da execução** narra o que aconteceu, em linhas com nível (`debug`, `info`, `warning`, `error`, `success`), enviadas incrementalmente e visíveis no app quase em tempo real. Ele é o único membro de `hobots` que **nunca lança**, justamente para poder ir num `finally` de limpeza. O servidor guarda até 1000 linhas por execução.
|
|
74
|
+
O **log da execução** narra o que aconteceu, em linhas com nível (`debug`, `info`, `warning`, `error`, `success`), enviadas incrementalmente e visíveis no app quase em tempo real (e ecoadas no terminal como `[hobots] INFO …`). Ele é o único membro de `hobots` que **nunca lança**, justamente para poder ir num `finally` de limpeza. O servidor guarda até 1000 linhas por execução.
|
|
75
75
|
|
|
76
76
|
## Anexo
|
|
77
77
|
|
|
78
|
-
Um **anexo** é uma imagem — na prática, um print — vinculada
|
|
78
|
+
Um **anexo** é uma imagem — na prática, um print — vinculada ao evento que ela ilustra: uma linha de log, um item ou uma step. Responde "como estava a tela?" quando texto não basta. Quem captura é o seu código (Playwright, Puppeteer, um arquivo); o SDK sobe direto para o S3 por URL presignada, em segundo plano, sem atrasar o log. Enquanto sobe, o app mostra "enviando imagem…".
|
|
79
79
|
|
|
80
80
|
## Cancelamento cooperativo
|
|
81
81
|
|
|
@@ -91,7 +91,7 @@ Um **evento** é a foto de um erro: tipo, mensagem, stack trace, tags, ambiente,
|
|
|
91
91
|
|
|
92
92
|
## Transaction
|
|
93
93
|
|
|
94
|
-
Uma **transaction** é uma unidade de trabalho **medida**: começa, termina, e o que fica é quanto tempo levou. Cada execução tem exatamente uma, aberta e finalizada pelo SDK (`hobots.transaction`), correlacionada à execução pelo `run_id`. Você não cria transactions — só adiciona etapas.
|
|
94
|
+
Uma **transaction** é uma unidade de trabalho **medida**: começa, termina, e o que fica é quanto tempo levou. Cada execução tem exatamente uma, chamada `run` (op `hobots.run`), aberta e finalizada pelo SDK (`hobots.transaction`), correlacionada à execução pelo `run_id`. Você não cria transactions — só adiciona etapas.
|
|
95
95
|
|
|
96
96
|
## Step
|
|
97
97
|
|
|
@@ -103,7 +103,7 @@ O **heartbeat** é o batimento "estou vivo", a cada 30 s. O servidor conhece a c
|
|
|
103
103
|
|
|
104
104
|
## instanceId
|
|
105
105
|
|
|
106
|
-
A identidade da **instância** do robô: qual VM, qual container. Vem da env `HOBOTS_INSTANCE_ID` (ou da opção; sem os dois, o hostname) e deve ser **estável entre reinícios** — um valor novo a cada boot vira uma frota de robôs fantasmas. É a mesma identidade no heartbeat, no poll e na criação de execuções: um agente só no painel.
|
|
106
|
+
A identidade da **instância** do robô: qual VM, qual container. Vem da env `HOBOTS_INSTANCE_ID` (ou da opção; sem os dois, o hostname) e deve ser **estável entre reinícios** — um valor novo a cada boot vira uma frota de robôs fantasmas. É a mesma identidade no heartbeat, no poll e na criação de execuções: um agente só no painel. A presença da instância também declara o **modo** (`on_demand` / `always_on`) e as colunas de itens do agente.
|
|
107
107
|
|
|
108
108
|
## Release e environment
|
|
109
109
|
|
|
@@ -111,7 +111,7 @@ A identidade da **instância** do robô: qual VM, qual container. Vem da env `HO
|
|
|
111
111
|
|
|
112
112
|
## Claim
|
|
113
113
|
|
|
114
|
-
O **claim** é o ato de um agente reivindicar uma execução pendente. É **server-side e atômico** (`FOR UPDATE SKIP LOCKED`): dois agentes que fazem poll no mesmo instante recebem conjuntos disjuntos — nenhum lock vive no seu processo. O agente pede só o que consegue rodar (
|
|
114
|
+
O **claim** é o ato de um agente reivindicar uma execução pendente. É **server-side e atômico** (`FOR UPDATE SKIP LOCKED`): dois agentes que fazem poll no mesmo instante recebem conjuntos disjuntos — nenhum lock vive no seu processo. O agente pede só o que consegue rodar (`max` = slots livres de `concurrency`), então nunca reivindica trabalho para deixar parado.
|
|
115
115
|
|
|
116
116
|
## Drain mode
|
|
117
117
|
|
package/docs/getting-started.md
CHANGED
|
@@ -12,8 +12,8 @@ Sem dependências de runtime. Requer **Node 18+** (usa o `fetch` global).
|
|
|
12
12
|
|
|
13
13
|
| Conceito | Em uma frase |
|
|
14
14
|
| --- | --- |
|
|
15
|
-
| [
|
|
16
|
-
| [Execução (run)](./concepts.md#execução-run) | Uma rodada
|
|
15
|
+
| [Agente](./concepts.md#agente) | O seu robô — um processo, uma coisa que ele faz, iniciado com `register` (sob demanda) ou `start` (sempre ligado) |
|
|
16
|
+
| [Execução (run)](./concepts.md#execução-run) | Uma rodada do agente, com logs, itens, progresso e resultado — um trabalho no app |
|
|
17
17
|
| [Item](./concepts.md#item) | Uma unidade de trabalho entregue pela execução (nota baixada, boleto emitido) |
|
|
18
18
|
| [Heartbeat](./concepts.md#heartbeat) | Batimento "estou vivo" — sempre ligado; o servidor acusa offline quando para |
|
|
19
19
|
| [instanceId](./concepts.md#instanceid) | A identidade da instância do robô no painel |
|
|
@@ -21,24 +21,24 @@ Sem dependências de runtime. Requer **Node 18+** (usa o `fetch` global).
|
|
|
21
21
|
|
|
22
22
|
## Credenciais
|
|
23
23
|
|
|
24
|
-
Cada agente tem **um Client Secret** (`hb_<64 hex>`), copiado em **Configurações → Agentes**. O SDK lê do ambiente
|
|
24
|
+
Cada agente tem **um Client Secret** (`hb_<64 hex>`), copiado em **Configurações → Agentes**. O SDK lê do ambiente:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
export HOBOTS_CLIENT_SECRET=hb_<64 hex>
|
|
28
28
|
export HOBOTS_INSTANCE_ID=vm-cliente-01 # opcional — sem ela, o hostname
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Também dá para passar `clientSecret`/`instanceId` nas opções
|
|
31
|
+
Também dá para passar `clientSecret`/`instanceId` nas opções de `register()`/`start()` (o snippet do app já vem assim). Em desenvolvimento, `HOBOTS_API_URL=http://localhost:3001` aponta para outra API; em produção é sempre `https://api2.hobots.app`.
|
|
32
32
|
|
|
33
|
-
##
|
|
33
|
+
## Primeiro agente sob demanda (`register`)
|
|
34
34
|
|
|
35
|
-
1. No app,
|
|
35
|
+
1. No app, abra o agente em **Configurações → Agentes** e monte o **formulário do agente** (os campos que cada solicitação vai carregar).
|
|
36
36
|
2. No robô:
|
|
37
37
|
|
|
38
38
|
```js
|
|
39
39
|
import hobots from 'hobots';
|
|
40
40
|
|
|
41
|
-
hobots.register(
|
|
41
|
+
hobots.register(async params => {
|
|
42
42
|
hobots.log.info(`lote ${params.lote}`);
|
|
43
43
|
for (const nota of params.notas) {
|
|
44
44
|
await baixar(nota);
|
|
@@ -50,14 +50,14 @@ hobots.register('processar-nfe', async params => {
|
|
|
50
50
|
process.on('SIGINT', () => void hobots.close(15_000).then(() => process.exit(0)));
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
3. Rode o robô: ele aparece online em **Instâncias** e fica aguardando. Crie uma solicitação pelo formulário — a execução aparece em **
|
|
53
|
+
3. Rode o robô: ele aparece online em **Instâncias** (modo "sob demanda") e fica aguardando. Crie uma solicitação pelo formulário — a execução aparece em **Trabalhos** com logs em tempo real.
|
|
54
54
|
|
|
55
|
-
##
|
|
55
|
+
## Primeiro agente sempre ligado (`start`)
|
|
56
56
|
|
|
57
57
|
```js
|
|
58
58
|
import hobots from 'hobots';
|
|
59
59
|
|
|
60
|
-
hobots.start(
|
|
60
|
+
hobots.start(async () => {
|
|
61
61
|
hobots.log.info('monitor iniciado');
|
|
62
62
|
while (true) {
|
|
63
63
|
await hobots.sleep(60_000); // cancelável — para se a execução for cancelada no app
|
|
@@ -66,10 +66,12 @@ hobots.start('monitor-pasta', async () => {
|
|
|
66
66
|
});
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
O robô abre a própria execução ao iniciar e ela aparece em **
|
|
69
|
+
O robô abre a própria execução ao iniciar e ela aparece em **Trabalhos** com origem "agente". Quando o handler retorna (ou é cancelado), a execução fecha; o processo continua vivo pelo heartbeat até `hobots.close()`.
|
|
70
|
+
|
|
71
|
+
> **Uma chamada por processo.** `register()` ou `start()`, nunca os dois — a segunda chamada lança. Um robô que faz duas coisas são dois agentes (dois processos, dois Client Secrets).
|
|
70
72
|
|
|
71
73
|
## Encerramento
|
|
72
74
|
|
|
73
75
|
`hobots.close(timeoutMs?)` para o polling, espera as execuções em voo, para o heartbeat e faz o flush final. Sem handler próprio de `SIGINT`/`SIGTERM`, o SDK reporta as execuções ativas como `AgentTerminated` antes do processo morrer.
|
|
74
76
|
|
|
75
|
-
Próximos passos: [
|
|
77
|
+
Próximos passos: [Execuções](./runs.md) para itens, progresso, anexos, cancelamento e retry; [Referência de API](./api-reference.md) para todas as opções.
|
package/docs/protocol.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
O SDK é um **cliente HTTP puro**, sem dependências de runtime: ele só fala com o app da Hobots por HTTP. Não abre conexão com banco e **não** roda `SKIP LOCKED` — a reivindicação atômica de runs é server-side (veja o fim desta página).
|
|
4
4
|
|
|
5
|
-
Os arquivos `src/protocol.ts` e `src/tasks/protocol.ts` são a **fonte de verdade do wire format**: o app valida os payloads com zod travados nesses tipos (`satisfies z.ZodType<...>`), então não podem sofrer drift. Todos os tipos são exportados por `hobots/protocol` (e reexportados pelo import raiz `hobots`).
|
|
5
|
+
Os arquivos `src/protocol.ts` e `src/tasks/protocol.ts` são a **fonte de verdade do wire format**: o app valida os payloads com zod travados nesses tipos (`satisfies z.ZodType<...>`), então não podem sofrer drift. Todos os tipos são exportados por `hobots/protocol` (inclusive `InstanceMode` e `ItemsDeclaration`) (e reexportados pelo import raiz `hobots`).
|
|
6
6
|
|
|
7
7
|
## O Client Secret e os endpoints derivados
|
|
8
8
|
|
|
9
|
-
Cada agente tem **um único Client Secret**, o mesmo para as duas capabilities:
|
|
9
|
+
Cada agente tem **um único Client Secret**, o mesmo para as duas capabilities. O agente é a própria tarefa: não há slug de task no wire — o que o agente faz é identificado pelo secret; a instância só declara **como** trabalha (`mode`).
|
|
10
10
|
|
|
11
11
|
```
|
|
12
12
|
hb_<64 hex>
|
|
@@ -15,10 +15,10 @@ hb_<64 hex>
|
|
|
15
15
|
| Canal | Endpoint | Autenticação |
|
|
16
16
|
| --- | --- | --- |
|
|
17
17
|
| Telemetria (ingestão) | `POST <baseUrl>/v1/ingest` | `Authorization: Bearer <clientSecret>` |
|
|
18
|
-
|
|
|
18
|
+
| Execuções (poll, runs, itens, anexos) | `<baseUrl>/v1/runs/*` | `Authorization: Bearer <clientSecret>` |
|
|
19
19
|
| Anexos (binário) | URL presignada do S3 (PUT) | assinatura na própria URL — **sem** header `Authorization` |
|
|
20
20
|
|
|
21
|
-
A credencial vem da opção `clientSecret` ou da env `HOBOTS_CLIENT_SECRET` (resolvida em `src/config.ts` no primeiro `register()`/`start()`, com a URL base `https://api2.hobots.app`). Em desenvolvimento a env `HOBOTS_API_URL` sobrescreve a URL base — apenas `http(s)`, e `http` para host fora de loopback gera aviso porque o secret trafega sem TLS. A credencial é **secreta** (formato `hb_` + 64 hex — outro formato é rejeitado
|
|
21
|
+
A credencial vem da opção `clientSecret` ou da env `HOBOTS_CLIENT_SECRET` (resolvida em `src/config.ts` no primeiro `register()`/`start()`, com a URL base `https://api2.hobots.app`). Em desenvolvimento a env `HOBOTS_API_URL` sobrescreve a URL base — apenas `http(s)`, e `http` para host fora de loopback gera aviso porque o secret trafega sem TLS. A credencial é **secreta** (formato `hb_` + 64 hex — outro formato é rejeitado no `register()`/`start()`, sem ecoar o valor) e viaja **só no header** `Authorization: Bearer` — nunca na URL da requisição.
|
|
22
22
|
|
|
23
23
|
## Telemetria — ingestão
|
|
24
24
|
|
|
@@ -47,7 +47,7 @@ Todo `transaction` carrega `run_id`: é assim que a [transaction da execução](
|
|
|
47
47
|
|
|
48
48
|
### MachineSnapshot
|
|
49
49
|
|
|
50
|
-
Recursos da máquina onde a instância roda. Vai em `HeartbeatPayload.machine`, em `instance.machine` do `POST /v1/
|
|
50
|
+
Recursos da máquina onde a instância roda. Vai em `HeartbeatPayload.machine`, em `instance.machine` do `POST /v1/runs/poll` e em `contexts.machine` dos eventos. Bytes em bytes, percentuais 0..100; campo ausente = não mensurável nesta plataforma. Teto: **4 KiB** serializado (heartbeat/poll → 422; `contexts.machine` idem). Chaves desconhecidas são descartadas pelo servidor.
|
|
51
51
|
|
|
52
52
|
| Campo | Tipo | Descrição |
|
|
53
53
|
| --- | --- | --- |
|
|
@@ -66,11 +66,11 @@ Recursos da máquina onde a instância roda. Vai em `HeartbeatPayload.machine`,
|
|
|
66
66
|
|
|
67
67
|
O servidor guarda o último snapshot em `instances.machine` e um ponto por heartbeat/poll (máx. 1 a cada 30s por instância) em `instance_metrics`, expurgado em 14 dias.
|
|
68
68
|
|
|
69
|
-
##
|
|
69
|
+
## Execuções — polling, criação e reporte
|
|
70
70
|
|
|
71
|
-
Endpoints sob `<baseUrl>/v1/
|
|
71
|
+
Endpoints sob `<baseUrl>/v1/runs`, todos com `Authorization: Bearer <clientSecret>` (a credencial única do agente):
|
|
72
72
|
|
|
73
|
-
### `POST /v1/
|
|
73
|
+
### `POST /v1/runs/poll`
|
|
74
74
|
|
|
75
75
|
Corpo `PollRequest`:
|
|
76
76
|
|
|
@@ -82,32 +82,31 @@ Corpo `PollRequest`:
|
|
|
82
82
|
"version": "3.0.0",
|
|
83
83
|
"host": "vm-01",
|
|
84
84
|
"environment": "production", // ambiente da instância: development | testing | staging | production
|
|
85
|
-
"
|
|
85
|
+
"mode": "on_demand", // como a instância trabalha: on_demand (register) | always_on (start)
|
|
86
86
|
"poll_interval_ms": 2000,
|
|
87
|
-
// colunas de itens declaradas no código (opção `items`) —
|
|
87
|
+
// colunas de itens declaradas no código (opção `items`) — uma declaração, do agente;
|
|
88
88
|
// ausente = nada declarado (o servidor nunca apaga a declaração anterior por ausência)
|
|
89
|
-
"
|
|
89
|
+
"items": { "attributes": [{ "key": "numero_nota" }, { "key": "valor", "label": "valor (R$)" }] }
|
|
90
90
|
},
|
|
91
|
-
"max": 2
|
|
92
|
-
"tasks": ["processar-nfe"] // só tasks de register() com slot livre — a concorrência é por task
|
|
91
|
+
"max": 2 // slots livres de `concurrency` (1..5)
|
|
93
92
|
}
|
|
94
93
|
```
|
|
95
94
|
|
|
96
|
-
Resposta `PollResponse` = `{ runs: RunDescriptor[] }`, cada `{ id,
|
|
95
|
+
Resposta `PollResponse` = `{ runs: RunDescriptor[] }`, cada `{ id, params, form, source, created_at }`.
|
|
97
96
|
|
|
98
|
-
O `instance.instance_id` é a **mesma identidade** do heartbeat de telemetria (`instance_id`): o servidor correlaciona os dois canais numa tabela `instances` por `(agent_id, instance_id)`. Os dois derivam do mesmo `instanceId` — o robô aparece como um agente só no painel. O próprio poll também marca a presença da instância (`last_poll_at`). Um processo
|
|
97
|
+
O `instance.instance_id` é a **mesma identidade** do heartbeat de telemetria (`instance_id`): o servidor correlaciona os dois canais numa tabela `instances` por `(agent_id, instance_id)`. Os dois derivam do mesmo `instanceId` — o robô aparece como um agente só no painel. O próprio poll também marca a presença da instância (`last_poll_at`). Um processo no modo `start()` nunca faz poll.
|
|
99
98
|
|
|
100
99
|
**Backoff:** um request por poll; o loop decide o ritmo. HTTP 401 (key rejeitada) → retry lento a 30s com log throttled; outros erros → ladder `[1s, 4s, 16s]`.
|
|
101
100
|
|
|
102
|
-
### `POST /v1/
|
|
101
|
+
### `POST /v1/runs`
|
|
103
102
|
|
|
104
|
-
Modo `start`: a instância abre a **própria run**. Corpo `RunCreateRequest` — `{ "
|
|
103
|
+
Modo `start`: a instância abre a **própria run**. Corpo `RunCreateRequest` — `{ "instance": { /* InstanceInfo (com `mode: "always_on"`), obrigatório */ } }`. Resposta `201` `RunCreateResponse` = o mesmo `RunDescriptor` do poll, com `params: {}`, `form: null` e `source: 'agent'`.
|
|
105
104
|
|
|
106
105
|
- A run nasce **`running`** (`started_at`, `claimed_at` e `last_report_at` = agora, `claim_count` 1), sem formulário nem solicitante, vinculada à instância informada. Dali em diante é reportada pelas rotas abaixo exatamente como uma run reivindicada no poll.
|
|
107
|
-
- `touchInstance` roda antes do insert (presença); se falhar, a API responde `503` e o SDK insiste com o ladder do poll (o handler só começa quando a run existe). Corpo vazio
|
|
106
|
+
- `touchInstance` roda antes do insert (presença); se falhar, a API responde `503` e o SDK insiste com o ladder do poll (o handler só começa quando a run existe). Corpo vazio ou sem `instance.instance_id` → `422`.
|
|
108
107
|
- Sem dedupe: um processo reiniciado em menos de 120 s deixa a run anterior `running` até o sweep marcá-la `AgentTerminated`. O app não oferece "reexecutar" para runs `agent` — ninguém faria poll por elas.
|
|
109
108
|
|
|
110
|
-
### `PATCH /v1/
|
|
109
|
+
### `PATCH /v1/runs/{id}`
|
|
111
110
|
|
|
112
111
|
Corpo `RunUpdateRequest`:
|
|
113
112
|
|
|
@@ -134,7 +133,7 @@ Corpo `RunUpdateRequest`:
|
|
|
134
133
|
|
|
135
134
|
Ciclo de vida completo da run no servidor (`RunStatus`): `pending → claimed → running → succeeded | failed | canceled` (runs `agent` nascem em `running`). O agente só transiciona para `running`/`succeeded`/`failed` (`RunUpdateStatus`).
|
|
136
135
|
|
|
137
|
-
### `POST /v1/
|
|
136
|
+
### `POST /v1/runs/{id}/items`
|
|
138
137
|
|
|
139
138
|
Registra em lote os **itens de execução** (a entrega do bot — `hobots.items` no SDK). Insert-only: cada item chega com o status final; um `id` repetido na mesma run é ignorado (retry de rede idempotente).
|
|
140
139
|
|
|
@@ -162,7 +161,7 @@ Resposta `200` `RunItemsCreateResponse`: `{ "accepted": 1, "duplicates": 0 }`
|
|
|
162
161
|
|
|
163
162
|
- **Run cancelada/terminal aceita itens** (200): item é fato de trabalho já feito — mesmo racional do log tardio. Esta rota não tem 409.
|
|
164
163
|
- O insert e os **contadores materializados** da run (`items_total/succeeded/failed/occurrence`, que alimentam os totalizadores no app) saem num único statement atômico no servidor.
|
|
165
|
-
- Retry `[1s, 4s, 16s]` para rede/5xx; `404` = servidor sem o recurso (o SDK degrada em silêncio); `
|
|
164
|
+
- Retry `[1s, 4s, 16s]` para rede/5xx; `404` = servidor sem o recurso (o SDK degrada em silêncio); `402` = armazenamento do plano esgotado. Não há teto de itens por run.
|
|
166
165
|
|
|
167
166
|
| Status | Quando |
|
|
168
167
|
| --- | --- |
|
|
@@ -170,9 +169,9 @@ Resposta `200` `RunItemsCreateResponse`: `{ "accepted": 1, "duplicates": 0 }`
|
|
|
170
169
|
| `404` | execução inexistente ou de outro projeto |
|
|
171
170
|
| `413` | corpo acima de 1 MB |
|
|
172
171
|
| `422` | payload inválido |
|
|
173
|
-
| `
|
|
172
|
+
| `402` | armazenamento do plano esgotado (`storage_limit`) |
|
|
174
173
|
|
|
175
|
-
### `POST /v1/
|
|
174
|
+
### `POST /v1/runs/{id}/attachments`
|
|
176
175
|
|
|
177
176
|
Registra um anexo de imagem e devolve uma **URL presignada de PUT**. O binário nunca passa pela API: o agente sobe direto no S3.
|
|
178
177
|
|
|
@@ -213,7 +212,7 @@ O PUT no S3 precisa mandar exatamente esses headers — eles fazem parte da assi
|
|
|
213
212
|
| `503` | anexos não habilitados nesta instalação (sem bucket S3 configurado) |
|
|
214
213
|
| `502` | falha ao presignar — o registro é desfeito |
|
|
215
214
|
|
|
216
|
-
### `PATCH /v1/
|
|
215
|
+
### `PATCH /v1/runs/{id}/attachments/{attachmentId}`
|
|
217
216
|
|
|
218
217
|
Confirma o upload. Corpo `RunAttachmentConfirmRequest`: `{ "status": "uploaded" | "failed", "size_bytes"?, "width"?, "height"? }`; resposta `{ "ok": true }`, ou `404` se o anexo não existe no projeto da chave.
|
|
219
218
|
|
|
@@ -223,7 +222,7 @@ Limites de anexo (aplicados no SDK **e** no servidor): PNG/JPEG/WEBP, 5 MB por i
|
|
|
223
222
|
|
|
224
223
|
## Onde vive o claim atômico
|
|
225
224
|
|
|
226
|
-
O `POST /v1/
|
|
225
|
+
O `POST /v1/runs/poll` no app chama `claimPendingRuns()` em
|
|
227
226
|
[`apps/api/lib/tasks/claim.ts`](../../../apps/api/lib/tasks/claim.ts): um CTE que seleciona runs `pending` vencidas com `FOR UPDATE SKIP LOCKED` e as promove a `claimed` num único `UPDATE ... RETURNING`. Isso garante que dois agentes concorrentes nunca peguem a mesma run. **Nada disso está no SDK** — ele só faz o HTTP.
|
|
228
227
|
|
|
229
228
|
## Identidade do SDK
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Execuções
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
O **agente** é o seu robô, e o handler que você passa a `register()`/`start()` é o que ele faz. Cada rodada é uma **execução** (run) na plataforma, com logs, itens, progresso, anexos e resultado. Há dois modos de iniciar o agente; tudo o mais (o objeto `hobots`, cancelamento, retry) é igual nos dois.
|
|
4
4
|
|
|
5
5
|
## Os dois modos
|
|
6
6
|
|
|
7
7
|
```js
|
|
8
|
-
// sob demanda: uma execução por solicitação do app (formulário
|
|
9
|
-
hobots.register(
|
|
8
|
+
// sob demanda: uma execução por solicitação do app (formulário do agente ou agendamento)
|
|
9
|
+
hobots.register(async params => { /* … */ }, { concurrency: 2 });
|
|
10
10
|
|
|
11
11
|
// sempre ligado: o robô abre a própria execução e roda o handler (o seu loop) uma vez
|
|
12
|
-
hobots.start(
|
|
12
|
+
hobots.start(async () => { /* while (true) { … } */ });
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
- `register` — o robô faz polling na plataforma; cada solicitação vira uma execução com os `params` do formulário.
|
|
16
|
-
- `start` — ao iniciar, o SDK cria a execução (`POST /v1/
|
|
17
|
-
-
|
|
15
|
+
- `register` — o robô faz polling na plataforma; cada solicitação vira uma execução com os `params` do formulário. O polling começa sozinho.
|
|
16
|
+
- `start` — ao iniciar, o SDK cria a execução (`POST /v1/runs`) e roda o handler com `params = {}`. Enquanto a plataforma não responde, insiste com backoff — o handler só começa quando a execução existe. Quando o handler retorna, a execução fecha como `succeeded` (ou `failed` após os retries) e o processo segue vivo pelo heartbeat. **Uma execução por processo** — para "recomeçar", reinicie o robô.
|
|
17
|
+
- **Uma chamada por processo**: `register` ou `start`. A segunda lança (`o agente já foi iniciado`). Dois trabalhos diferentes são dois agentes.
|
|
18
18
|
|
|
19
|
-
> No modo start, lembre do teto de **1000 linhas de log por execução** no servidor: logue novidades e use `hobots.items` para o trabalho feito (
|
|
19
|
+
> No modo start, lembre do teto de **1000 linhas de log por execução** no servidor: logue novidades e use `hobots.items` para o trabalho feito (sem teto por execução — só o armazenamento do plano limita).
|
|
20
20
|
|
|
21
21
|
## O handler e o objeto `hobots`
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
|
-
type
|
|
24
|
+
type RunHandler<P = Record<string, unknown>> = (params: P, ctx: RunContext) => void | Promise<void>;
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
Dentro do handler — e em **qualquer módulo** chamado por ele — o objeto global `hobots` resolve a execução atual:
|
|
@@ -40,15 +40,15 @@ Isso funciona com `concurrency > 1` porque cada execução roda no seu contexto
|
|
|
40
40
|
|
|
41
41
|
| Membro | Notas |
|
|
42
42
|
| --- | --- |
|
|
43
|
-
| `hobots.log.<debug\|info\|warn\|error\|success>(msg, ...rest)` | Enviado incrementalmente (PATCHes com throttle) e ecoado no terminal. Nunca lança. `hobots.image(...)` no último argumento = print da linha. |
|
|
43
|
+
| `hobots.log.<debug\|info\|warn\|error\|success>(msg, ...rest)` | Enviado incrementalmente (PATCHes com throttle) e ecoado no terminal (`[hobots] INFO …`). Nunca lança. `hobots.image(...)` no último argumento = print da linha. |
|
|
44
44
|
| `hobots.items` | Ver [Itens](#itens-de-execução-hobotsitems) |
|
|
45
45
|
| `hobots.progress` | Ver [Progresso](#progresso-hobotsprogress) |
|
|
46
|
-
| `hobots.transaction` | Transaction da execução, já correlacionada e finalizada pelo SDK. `startChild(op, descrição)` = uma step no waterfall. |
|
|
46
|
+
| `hobots.transaction` | Transaction da execução (`run`), já correlacionada e finalizada pelo SDK. `startChild(op, descrição)` = uma step no waterfall. |
|
|
47
47
|
| `hobots.image(imagem, opts?)` | Ver [Imagens](#imagens-screenshots) |
|
|
48
48
|
| `hobots.captureException(erro, hint?)` | Issue no módulo Issues; `hint.attachment` anexa um print à linha de log |
|
|
49
49
|
| `hobots.retry(fn, opts?)` | Ver [Retry](#retry) |
|
|
50
50
|
| `hobots.sleep(ms)` · `hobots.signal` · `hobots.throwIfCanceled()` | Ver [Cancelamento](#cancelamento-cooperativo) |
|
|
51
|
-
| `hobots.runId` · `hobots.
|
|
51
|
+
| `hobots.runId` · `hobots.params` | Identidade da execução |
|
|
52
52
|
|
|
53
53
|
- **Retorno** do handler: **ignorado** (resultados podem conter dados sensíveis). Reporte por logs e itens.
|
|
54
54
|
- **Erro lançado**: nova tentativa (ver Retry) e, esgotadas, `failed` com tipo, mensagem (cap 10k) e stack (cap 20k), passando pelo scrub de segredos.
|
|
@@ -57,7 +57,7 @@ Isso funciona com `concurrency > 1` porque cada execução roda no seu contexto
|
|
|
57
57
|
|
|
58
58
|
## Itens de execução (`hobots.items`)
|
|
59
59
|
|
|
60
|
-
A **entrega do robô**: cada item processado, com status e atributos livres (viram colunas na aba de itens da execução no app), totalizadores
|
|
60
|
+
A **entrega do robô**: cada item processado, com status e atributos livres (viram colunas na aba de itens da execução no app), totalizadores no trabalho.
|
|
61
61
|
|
|
62
62
|
```js
|
|
63
63
|
hobots.items.succeeded({ numero_nota: nota.numero, cnpj: nota.cnpj }, { id: nota.numero });
|
|
@@ -68,7 +68,7 @@ hobots.items.add({ status: 'succeeded', id: '11', payload: { … } });
|
|
|
68
68
|
|
|
69
69
|
- Chamadas síncronas e bufferizadas (lotes de até 500); o SDK drena o buffer antes do reporte final.
|
|
70
70
|
- `id` é a identidade de negócio do item na execução — repetido, **vale o último reporte** (o servidor faz upsert: uma conta que falhou numa tentativa e deu certo no retry fica como sucesso; retry de rede é no-op). Sem `id`, cada chamada é um item novo.
|
|
71
|
-
- Limites:
|
|
71
|
+
- Limites: sem teto de itens por execução (só o armazenamento do plano), payload 16 KB, mensagem 2.000 caracteres. Uma execução cancelada ainda aceita os itens já bufferizados (trabalho feito).
|
|
72
72
|
- Cada chamada é um **ponto de parada cooperativo** (lança `RunCanceled` se a execução foi cancelada).
|
|
73
73
|
|
|
74
74
|
### Processar uma lista inteira (`hobots.items.each`)
|
|
@@ -96,22 +96,50 @@ const resultado = await hobots.items.each(
|
|
|
96
96
|
- Só **cancelamento, timeout e shutdown** interrompem o loop: o `RunCanceled` sobe, o item em curso não é registrado e o progresso não avança.
|
|
97
97
|
- Aceita `Iterable` ou `AsyncIterable` (sem `length`, nenhum total é declarado).
|
|
98
98
|
|
|
99
|
-
> Se o handler
|
|
99
|
+
> Se o handler termina com o progresso parado antes do total declarado (ex.: 45/76), o SDK deixa um aviso no log da execução — e a Central sinaliza o desencontro entre processados e itens reportados.
|
|
100
100
|
|
|
101
101
|
### Colunas da aba de itens
|
|
102
102
|
|
|
103
|
-
As colunas vêm do código, não do formulário —
|
|
103
|
+
As colunas vêm do código, não do formulário — é **uma declaração só, do agente**, válida para `register()` (com ou sem formulário) e `start()`:
|
|
104
104
|
|
|
105
105
|
```js
|
|
106
|
-
register(
|
|
106
|
+
hobots.register(handler, {
|
|
107
107
|
items: { columns: ['numero_nota', { key: 'cnpj', label: 'CNPJ' }, { key: 'valor', label: 'valor (R$)' }] },
|
|
108
108
|
});
|
|
109
109
|
```
|
|
110
110
|
|
|
111
111
|
- Ordem da lista = ordem das colunas; até 12; chave com letras, números, `.`, `-`, `_` (inválidas são descartadas com aviso em `debug`).
|
|
112
|
-
- A declaração vai na presença da instância (poll / abertura de run). No app, **Configurações → agente → itens da execução** pode personalizar as colunas (e restaurar as do código).
|
|
112
|
+
- A declaração vai na presença da instância (`instance.items` no poll / abertura de run). No app, **Configurações → agente → itens da execução** pode personalizar as colunas (e restaurar as do código).
|
|
113
113
|
- Sem declaração nem personalização, o app mostra as chaves encontradas nos próprios payloads.
|
|
114
114
|
|
|
115
|
+
## Entregar enquanto processa (`hobots.flush()`)
|
|
116
|
+
|
|
117
|
+
A transaction da execução só é enviada quando a run termina — num agente sempre ligado (`start()`) isso é nunca. `hobots.flush()` deixa o **bot** decidir quando entregar o que já fez:
|
|
118
|
+
|
|
119
|
+
- cada step **raiz** já finalizada (com todas as filhas; as filhas ainda abertas são fechadas) vai numa transaction parcial da mesma run (`op` `hobots.run.flush`, `event_id` novo, até 200 steps cada) e sai da memória do SDK; steps raiz ainda abertas ficam para o próximo flush;
|
|
120
|
+
- itens, logs e progresso pendentes saem já, sem esperar o throttle;
|
|
121
|
+
- aguarda a entrega com teto (`timeoutMs`, default 15 s) e **nunca lança** — pode ir num `finally`.
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
hobots.start(async () => {
|
|
125
|
+
while (true) {
|
|
126
|
+
const lote = await fila.pegar();
|
|
127
|
+
if (lote.length) {
|
|
128
|
+
const rodada = hobots.transaction.startChild('rodada', `${lote.length} pedido(s)`);
|
|
129
|
+
try {
|
|
130
|
+
for (const pedido of lote) await processar(pedido, rodada); // steps filhas + hobots.items.*
|
|
131
|
+
} finally {
|
|
132
|
+
rodada.finish();
|
|
133
|
+
await hobots.flush(); // waterfall e itens desta rodada já na Central
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
await hobots.sleep(6_000);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Fila vazia → não abra step: cada step também vira duas linhas (START/FINISH) no log da execução, que guarda no máximo 1000.
|
|
142
|
+
|
|
115
143
|
## Progresso (`hobots.progress`)
|
|
116
144
|
|
|
117
145
|
```js
|
|
@@ -156,17 +184,17 @@ try {
|
|
|
156
184
|
hobots.captureException(erro, { attachment: hobots.image(await page.screenshot(), { caption: 'tela no momento do erro' }) });
|
|
157
185
|
```
|
|
158
186
|
|
|
159
|
-
O SDK não tira o print: aceita bytes (`Uint8Array`/`ArrayBuffer`/`Buffer`) ou um caminho de arquivo, crus ou embrulhados por `hobots.image(bytes, { caption?, contentType? })` (só o embrulho leva legenda; em `log.*` ele é obrigatório para distinguir a imagem de um argumento comum). O evento entra na hora com o id do anexo; o upload sobe em segundo plano direto para o S3 (URL presignada) e **nunca lança nem bloqueia** — falhas viram um aviso no log da execução. Limites: PNG/JPEG/WEBP, 5 MB, 20 por execução, 4 por evento. Fora de uma execução não há onde pendurar a imagem (descartada com aviso).
|
|
187
|
+
O SDK não tira o print: aceita bytes (`Uint8Array`/`ArrayBuffer`/`Buffer`) ou um caminho de arquivo, crus ou embrulhados por `hobots.image(bytes, { caption?, contentType? })` (só o embrulho leva legenda; em `log.*` ele é obrigatório para distinguir a imagem de um argumento comum). O evento entra na hora com o id do anexo; o upload sobe em segundo plano direto para o S3 (URL presignada) e **nunca lança nem bloqueia** — falhas viram um aviso no log da execução. Limites: PNG/JPEG/WEBP, 5 MB, 20 por execução, 4 por evento. Fora de uma execução não há onde pendurar a imagem (descartada com aviso).
|
|
160
188
|
|
|
161
189
|
## Cancelamento cooperativo
|
|
162
190
|
|
|
163
|
-
Cancelar
|
|
191
|
+
Cancelar o trabalho no app **não mata** o handler: o SDK detecta (o PATCH periódico recebe 409), dispara `hobots.signal` e o próximo **ponto de parada** lança `RunCanceled`:
|
|
164
192
|
|
|
165
193
|
- `hobots.items.*`, `hobots.progress.total/advance/set`, `hobots.transaction.startChild()`, `hobots.sleep()` e cada tentativa de `hobots.retry()`.
|
|
166
194
|
- **Nunca `hobots.log.*`** — log em `finally` de limpeza não pode explodir.
|
|
167
195
|
|
|
168
196
|
```js
|
|
169
|
-
hobots.register(
|
|
197
|
+
hobots.register(async params => {
|
|
170
198
|
const browser = await abrir();
|
|
171
199
|
try {
|
|
172
200
|
for (const nota of params.notas) {
|
|
@@ -200,7 +228,7 @@ const dados = await hobots.retry(() => portal.buscar(nota), { retries: 3, delayM
|
|
|
200
228
|
|
|
201
229
|
## Concorrência (`register`)
|
|
202
230
|
|
|
203
|
-
`concurrency` (1..5, default 1)
|
|
231
|
+
`concurrency` (1..5, default 1) é **do agente**: quantas solicitações ele atende ao mesmo tempo. O agente só pede à plataforma o que consegue rodar (`max` = slots livres, teto 5) e entra em **drain mode** — re-pesquisa imediatamente enquanto vem trabalho; sem trabalho, espera `pollIntervalMs`.
|
|
204
232
|
|
|
205
233
|
## Timeout
|
|
206
234
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hobots",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "SDK JavaScript da Hobots —
|
|
3
|
+
"version": "4.1.0",
|
|
4
|
+
"description": "SDK JavaScript da Hobots — o agente é a própria tarefa: register() atende solicitações da plataforma (sob demanda), start() roda sempre ligado e reporta logs, itens e progresso. Credenciais por env, heartbeat sempre ligado, objeto global `hobots`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"license": "MIT",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../protocol-limits.json","../src/wire-limits.ts"],"sourcesContent":["{\n \"MAX_ITEMS_PER_ENVELOPE\": 50,\n \"HEARTBEAT_DEFAULT_INTERVAL_MS\": 30000,\n \"HEARTBEAT_MIN_INTERVAL_MS\": 1000,\n \"HEARTBEAT_MAX_INTERVAL_MS\": 900000,\n \"MAX_CLAIM_PER_POLL\": 5,\n \"MAX_LOG_MESSAGE_CHARS\": 10000,\n \"MAX_LOGS_PER_PATCH\": 100,\n \"MAX_ERROR_MESSAGE_CHARS\": 10000,\n \"MAX_ERROR_STACK_CHARS\": 20000,\n \"MAX_ITEMS_PER_RUN\": 10000,\n \"MAX_ITEMS_PER_REQUEST\": 500,\n \"MAX_ITEM_MESSAGE_CHARS\": 2000,\n \"MAX_ITEM_PAYLOAD_BYTES\": 16384,\n \"MAX_ITEM_COLUMNS\": 12,\n \"MAX_ITEM_COLUMN_LABEL_CHARS\": 60,\n \"MAX_ATTACHMENT_BYTES\": 5242880,\n \"MAX_ATTACHMENTS_PER_RUN\": 20,\n \"MAX_ATTACHMENTS_PER_LOG_ENTRY\": 4,\n \"MAX_CAPTION_CHARS\": 200,\n \"MAX_IMAGE_DIMENSION_PX\": 20000\n}\n","import limits from '../protocol-limits.json';\n\n/**\n * Limites do wire SDK ↔ servidor — fonte ÚNICA (protocol-limits.json): o SDK\n * Node lê daqui, a @hobots/api importa de `hobots/protocol` e o SDK Python\n * confere a paridade num teste que lê o mesmo JSON. Mudar um limite = mudar o\n * JSON (e, no Python, a constante espelhada — o teste acusa).\n */\nexport const WIRE_LIMITS = limits as Readonly<typeof limits>;\nexport type WireLimits = typeof WIRE_LIMITS;\n"],"mappings":";AAAA;AAAA,EACE,wBAA0B;AAAA,EAC1B,+BAAiC;AAAA,EACjC,2BAA6B;AAAA,EAC7B,2BAA6B;AAAA,EAC7B,oBAAsB;AAAA,EACtB,uBAAyB;AAAA,EACzB,oBAAsB;AAAA,EACtB,yBAA2B;AAAA,EAC3B,uBAAyB;AAAA,EACzB,mBAAqB;AAAA,EACrB,uBAAyB;AAAA,EACzB,wBAA0B;AAAA,EAC1B,wBAA0B;AAAA,EAC1B,kBAAoB;AAAA,EACpB,6BAA+B;AAAA,EAC/B,sBAAwB;AAAA,EACxB,yBAA2B;AAAA,EAC3B,+BAAiC;AAAA,EACjC,mBAAqB;AAAA,EACrB,wBAA0B;AAC5B;;;ACbO,IAAM,cAAc;","names":[]}
|