hobots 4.2.0 → 5.0.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 +27 -11
- package/dist/cli.cjs +250 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/index.cjs +796 -246
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -229
- package/dist/index.d.ts +91 -229
- package/dist/index.js +796 -246
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +17 -3
- package/dist/protocol.d.ts +17 -3
- package/docs/README.md +2 -2
- package/docs/api-reference.md +25 -9
- package/docs/concepts.md +12 -12
- package/docs/getting-started.md +5 -3
- package/docs/protocol.md +16 -8
- package/docs/runs.md +97 -23
- package/package.json +4 -1
package/docs/runs.md
CHANGED
|
@@ -29,9 +29,11 @@ Dentro do handler — e em **qualquer módulo** chamado por ele — o objeto glo
|
|
|
29
29
|
```js
|
|
30
30
|
// notas.js — não recebe ctx nenhum
|
|
31
31
|
export async function baixar(nota) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
const xml = await hobots.step(`GET /notas/${nota}`, async etapa => {
|
|
33
|
+
const xml = await portal.baixar(nota); // lançou? o SDK tenta de novo uma vez
|
|
34
|
+
etapa.data('bytes', xml.length);
|
|
35
|
+
return xml;
|
|
36
|
+
});
|
|
35
37
|
hobots.items.succeeded({ numero_nota: nota }, { id: nota });
|
|
36
38
|
}
|
|
37
39
|
```
|
|
@@ -43,7 +45,7 @@ Isso funciona com `concurrency > 1` porque cada execução roda no seu contexto
|
|
|
43
45
|
| `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
46
|
| `hobots.items` | Ver [Itens](#itens-de-execução-hobotsitems) |
|
|
45
47
|
| `hobots.progress` | Ver [Progresso](#progresso-hobotsprogress) |
|
|
46
|
-
| `hobots.
|
|
48
|
+
| `hobots.step(nome, fn, opts?)` | Ver [Etapas](#etapas-hobotsstep) |
|
|
47
49
|
| `hobots.image(imagem, opts?)` | Ver [Imagens](#imagens-screenshots) |
|
|
48
50
|
| `hobots.captureException(erro, hint?)` | Issue no módulo Issues; `hint.attachment` anexa um print à linha de log |
|
|
49
51
|
| `hobots.retry(fn, opts?)` | Ver [Retry](#retry) |
|
|
@@ -52,6 +54,7 @@ Isso funciona com `concurrency > 1` porque cada execução roda no seu contexto
|
|
|
52
54
|
|
|
53
55
|
- **Retorno** do handler: **ignorado** (resultados podem conter dados sensíveis). Reporte por logs e itens.
|
|
54
56
|
- **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
|
+
- Para rodar o handler na sua máquina sem a Central, ver [Modo local](#modo-local-teste-sem-a-central).
|
|
55
58
|
|
|
56
59
|
> **Dados sensíveis:** logs, itens, erros e prints são enviados ao servidor e ficam visíveis no app.
|
|
57
60
|
|
|
@@ -79,7 +82,7 @@ O contrato da plataforma — **todo item previsto termina com um desfecho regist
|
|
|
79
82
|
const resultado = await hobots.items.each(
|
|
80
83
|
contas,
|
|
81
84
|
async (conta, item) => {
|
|
82
|
-
await login(conta); // lançou → item `failed` com a mensagem,
|
|
85
|
+
await login(conta); // lançou → item `failed` com a mensagem, etapa em erro, segue
|
|
83
86
|
const fatura = await baixar(conta);
|
|
84
87
|
if (!fatura) return item.occurrence('sem fatura no mês'); // ocorrência de negócio
|
|
85
88
|
item.succeeded({ fatura: fatura.numero, valor: fatura.valor }); // mescla com o payload base
|
|
@@ -90,8 +93,8 @@ const resultado = await hobots.items.each(
|
|
|
90
93
|
// resultado = { total, succeeded, failed, occurrence }
|
|
91
94
|
```
|
|
92
95
|
|
|
93
|
-
- Cada elemento vira uma **
|
|
94
|
-
- **Exceção no handler** = item `failed` com a mensagem do erro, `
|
|
96
|
+
- Cada elemento vira uma **etapa** no waterfall (`op` 'item', descrição = `id`; `hobots.step(...)` dentro do handler vira sub-etapa dela, e `item.step` é o handle `warning`/`image`/`data`) e um **item** com o `id` e o payload base.
|
|
97
|
+
- **Exceção no handler** = item `failed` com a mensagem do erro, etapa em `error`, linha de erro no log da run — e o loop **segue para o próximo**. Nada do que o handler já registrou (`item.failed/occurrence`) é sobrescrito. O `each` não repete o item: para tentar de novo um trecho, use `hobots.step` dentro do handler.
|
|
95
98
|
- **Progresso automático**: declara o total pelo tamanho da lista (se o bot ainda não declarou um) e avança a cada elemento, inclusive nos que falharam. `progress: false` deixa o progresso por conta do bot.
|
|
96
99
|
- 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
100
|
- Aceita `Iterable` ou `AsyncIterable` (sem `length`, nenhum total é declarado).
|
|
@@ -112,11 +115,41 @@ hobots.register(handler, {
|
|
|
112
115
|
- 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
116
|
- Sem declaração nem personalização, o app mostra as chaves encontradas nos próprios payloads.
|
|
114
117
|
|
|
118
|
+
## Etapas (`hobots.step`)
|
|
119
|
+
|
|
120
|
+
Uma **etapa** é um trecho da execução com nome de negócio, medido no waterfall da aba de performance. Uma chamada só: o SDK abre a etapa, roda a função, **tenta de novo** se ela lançar e fecha.
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
// retries = 1 e retryDelayMs = 5_000 por padrão
|
|
124
|
+
const pedido = await hobots.step('Buscar pedido', () => portal.buscar(numero));
|
|
125
|
+
|
|
126
|
+
// etapa não idempotente (enviar, pagar, emitir): sem repetir
|
|
127
|
+
await hobots.step(`Emitir nota ${numero}`, () => portal.emitir(numero), { retries: 0 });
|
|
128
|
+
|
|
129
|
+
// mais tentativas, espera própria (número ou função da tentativa, 1-based)
|
|
130
|
+
await hobots.step('Entrar no portal', login, { retries: 3, retryDelayMs: attempt => attempt * 2_000 });
|
|
131
|
+
|
|
132
|
+
// a função recebe o handle `etapa` para o que o SDK não vê sozinho
|
|
133
|
+
await hobots.step('Conferir anexos', async etapa => {
|
|
134
|
+
const anexos = await portal.anexos(numero);
|
|
135
|
+
etapa.data('total', anexos.length);
|
|
136
|
+
if (anexos.length === 0) etapa.warning('pedido sem anexos', { image: await page.screenshot() });
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- **Uma etapa só na Central**, cobrindo todas as tentativas: `data.attempts` quando houve mais de uma; status `warning` com o motivo (`data.message = "tentativa 1/2 falhou: Tipo: mensagem"`) quando a nova tentativa deu certo — a menos que a função já tenha marcado `warning`; `error` com `data.error = { type, message }` quando esgotou (a exceção original sobe); `cancelled` em cancelamento, timeout ou shutdown (nunca repetem).
|
|
141
|
+
- Cada tentativa falhada vai para o **log da run**: `etapa "Buscar pedido": tentativa 1/2 falhou: Error: portal caiu — nova tentativa em 5000ms`. A espera é cancelável: uma execução cancelada no app não espera a próxima tentativa.
|
|
142
|
+
- **Aninhamento automático**: `hobots.step` dentro da função vira sub-etapa da etapa corrente — inclusive dentro do handler de `items.each`, onde aninha na etapa do item.
|
|
143
|
+
- O handle `etapa` (`StepHandle`): `warning(motivo, { image }?)` (uma etapa com erro não volta para aviso), `image(png)` (até 4 por etapa; vai em `data.attachments`), `data(chave, valor)` (`attachments` é reservada). Nada de abrir, fechar ou mudar status — isso é do SDK.
|
|
144
|
+
- Abrir a etapa é um **ponto de parada** (`RunCanceled` se a execução foi cancelada). O nome é recortado em 2048 caracteres no cliente.
|
|
145
|
+
- Fora de uma execução (aviso único), `hobots.step` roda a função com o mesmo retry, sem registrar nada. No [modo local](#modo-local-teste-sem-a-central) a etapa fica só na memória e o FINISH ecoa no terminal: `[hobots:step] FINISH Buscar pedido (812ms): tentativa 1/2 falhou: …`.
|
|
146
|
+
- `hobots.retry(fn)` continua existindo para repetir **sem etapa**; `retries` de `register()`/`start()` repete o **trabalho inteiro**.
|
|
147
|
+
|
|
115
148
|
## Entregar enquanto processa (`hobots.flush()`)
|
|
116
149
|
|
|
117
150
|
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
151
|
|
|
119
|
-
- cada
|
|
152
|
+
- cada etapa **raiz** já finalizada (com todas as sub-etapas; as ainda abertas são fechadas) vai numa transaction parcial da mesma run (`op` `hobots.run.flush`, `event_id` novo, até 200 etapas cada) e sai da memória do SDK; etapas raiz ainda abertas ficam para o próximo flush;
|
|
120
153
|
- itens, logs e progresso pendentes saem já, sem esperar o throttle;
|
|
121
154
|
- aguarda a entrega com teto (`timeoutMs`, default 15 s) e **nunca lança** — pode ir num `finally`.
|
|
122
155
|
|
|
@@ -125,11 +158,11 @@ hobots.start(async () => {
|
|
|
125
158
|
while (true) {
|
|
126
159
|
const lote = await fila.pegar();
|
|
127
160
|
if (lote.length) {
|
|
128
|
-
const rodada = hobots.transaction.startChild('rodada', `${lote.length} pedido(s)`);
|
|
129
161
|
try {
|
|
130
|
-
|
|
162
|
+
await hobots.step(`rodada: ${lote.length} pedido(s)`, async () => {
|
|
163
|
+
for (const pedido of lote) await processar(pedido); // sub-etapas (hobots.step) + hobots.items.*
|
|
164
|
+
}, { retries: 0 });
|
|
131
165
|
} finally {
|
|
132
|
-
rodada.finish();
|
|
133
166
|
await hobots.flush(); // waterfall e itens desta rodada já na Central
|
|
134
167
|
}
|
|
135
168
|
}
|
|
@@ -138,7 +171,7 @@ hobots.start(async () => {
|
|
|
138
171
|
});
|
|
139
172
|
```
|
|
140
173
|
|
|
141
|
-
Fila vazia → não abra
|
|
174
|
+
Fila vazia → não abra etapa: cada etapa também vira duas linhas (START/FINISH) no log da execução, que guarda no máximo 1000.
|
|
142
175
|
|
|
143
176
|
## Progresso (`hobots.progress`)
|
|
144
177
|
|
|
@@ -182,9 +215,54 @@ for (const ref of hobots.files.list()) hobots.log.info(ref.name, ref.size);
|
|
|
182
215
|
|
|
183
216
|
`isFileRef(valor)` (export do pacote, também `hobots.isFileRef`) reconhece a forma de um `FileRef` — útil para percorrer params aninhados.
|
|
184
217
|
|
|
218
|
+
## Modo local (teste sem a Central)
|
|
219
|
+
|
|
220
|
+
Para testar o robô na sua máquina sem Client Secret, sem Central e sem rede, rode-o com a env **`HOBOTS_LOCAL_RUN=1`**. `register()` e `start()` se comportam igual neste modo: o handler roda **uma vez**, com `params = JSON.parse(HOBOTS_RUN_PARAMS || '{}')`, e o processo encerra. O código do bot não muda — `hobots.log`, `hobots.items`, `hobots.progress`, `hobots.files`, `items.each`, `hobots.step` (o FINISH de cada etapa ecoa como `[hobots:step] FINISH <nome> (123ms): <motivo>`), cancelamento e retry são os mesmos de produção; só o transporte é trocado por um que não fala com ninguém.
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
HOBOTS_LOCAL_RUN=1 HOBOTS_RUN_PARAMS='{"lote": 3}' node bot.js
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- `HOBOTS_CLIENT_SECRET`, `HOBOTS_RUN_ID` e `HOBOTS_API_URL` são ignoradas. Nenhuma requisição sai: sem heartbeat, sem poll, sem issues (`hobots.captureException` devolve `undefined`).
|
|
227
|
+
- `hobots.runId` é `'local'`; `HOBOTS_RUN_PARAMS` que não seja um objeto JSON válido imprime `[hobots] ERROR HOBOTS_RUN_PARAMS inválido: <motivo>` no stderr e encerra com código **2**.
|
|
228
|
+
- **Exit code** pelo desfecho: **0** handler terminou; **1** lançou (depois dos retries configurados) — no stderr sai `[hobots] ERROR execução falhou — <Tipo>: <mensagem>` e a stack completa; **130** cancelado — `SIGINT`/`SIGTERM` disparam `hobots.signal` na execução em voo e esperam o handler parar por até 4 s (com `installExitHandlers: false`, ou com handler próprio do programa para o sinal, o SDK não intervém).
|
|
229
|
+
|
|
230
|
+
O que sai no **stdout** (linhas estáveis, pensadas para quem lê a saída por script):
|
|
231
|
+
|
|
232
|
+
| Linha | Quando |
|
|
233
|
+
| --- | --- |
|
|
234
|
+
| `[hobots] INFO modo local — execução sem a Central; params de HOBOTS_RUN_PARAMS` | primeira linha, ao entrar no modo |
|
|
235
|
+
| `[hobots] INFO <mensagem>` (e `WARNING`/`ERROR` no stderr) | cada `hobots.log.*`, como sempre |
|
|
236
|
+
| `[hobots:progress] <current>/<total>` | `hobots.progress` (também fora do modo local) |
|
|
237
|
+
| `[hobots:item] <status>\t<id>\t<message>` | um item por linha, `status` = `succeeded` \| `occurrence` \| `failed`; tab e quebra de linha em id/mensagem viram espaço |
|
|
238
|
+
| `[hobots:summary] total=<n> ok=<n> avisos=<n> falhas=<n> pulados=0` | no fim, contagem dos itens por status |
|
|
239
|
+
| `[hobots] INFO execução local concluída (<succeeded\|failed\|canceled>) em <N>s` | última linha |
|
|
240
|
+
|
|
241
|
+
Imagens e anexos (`hobots.image`, `{ image }` em itens e steps) são **descartados**, com um aviso único: `imagens e anexos não são enviados no modo local (descartados)`.
|
|
242
|
+
|
|
243
|
+
**Arquivos.** `hobots.files` funciona com `storage: 'local'`; o formato `$HOBOTS_LOCAL_FILES_DIR/<id>/<nome>` continua valendo, e só no modo local o `FileRef` aceita a chave extra **`path`** (caminho absoluto ou relativo à pasta de trabalho): `download` copia de `path` **sem apagar a origem** (o teste roda quantas vezes quiser) e sem conferir `size`. `path` inexistente ou que não é arquivo regular: `arquivo "<nome>" não encontrado em <path>`. Um arquivo enviado pela Central (`storage: 's3'`) não existe aqui: `arquivo "<nome>" enviado pela Central não está disponível no modo local`. Fora do modo local a chave `path` é ignorada.
|
|
244
|
+
|
|
245
|
+
### A CLI `hobots run`
|
|
246
|
+
|
|
247
|
+
A CLI monta as envs e os params por você — e roda o entry com o **mesmo Node** (`process.execPath`), na mesma pasta de trabalho, herdando o terminal; o exit code do bot é o da CLI:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
npx hobots run bot.js --params entrada.json --param lote=3 --param nome='"ACME"' --file planilha=./dados.xlsx -- --verbose
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
| Opção | Efeito |
|
|
254
|
+
| --- | --- |
|
|
255
|
+
| `--params <arquivo.json>` | objeto JSON base dos params |
|
|
256
|
+
| `--param <chave>=<valor>` (repetível) | valor parseado como JSON quando for JSON válido (`3`, `true`, `"x"`, `[1]`), senão string crua; sobrescreve `--params` |
|
|
257
|
+
| `--file <campo>=<caminho>` (repetível) | vira um `FileRef` `{ id: <uuid>, name, size, contentType, storage: 'local', path: <caminho absoluto> }`; o mesmo campo repetido vira uma lista; `contentType` pela extensão (`xlsx`, `xls`, `csv`, `ods`, `pdf`, `json`, `txt`, `xml`, `png`, `jpg`/`jpeg`, `zip`; demais `application/octet-stream`); caminho inexistente → `arquivo não encontrado: <caminho>`, exit 2 |
|
|
258
|
+
| `--reset-ledger` | define `HOBOTS_LEDGER_RESET=1` no ambiente do bot (o ledger é do esqueleto do bot, não do SDK) |
|
|
259
|
+
| `-- <args>` | tudo depois de `--` vai para o bot |
|
|
260
|
+
|
|
261
|
+
`--help`/`-h` imprime a linha de uso no stdout (exit 0). Uso errado (sem entry, flag desconhecida, `--param` sem `=`): mensagem curta + linha de uso no stderr e exit **2**; entry inexistente → `arquivo não encontrado: <entry>`; `--params` com arquivo inexistente, JSON inválido ou que não é objeto → `--params: <motivo>` (exit 2). A CLI repassa `SIGINT`/`SIGTERM` ao bot e espera ele encerrar. O SDK Python tem a mesma CLI: `python -m hobots run bot.py …`.
|
|
262
|
+
|
|
185
263
|
## Imagens (screenshots)
|
|
186
264
|
|
|
187
|
-
Um print vai **junto do evento** que ele ilustra — a linha de log, o item ou a
|
|
265
|
+
Um print vai **junto do evento** que ele ilustra — a linha de log, o item ou a etapa — para o
|
|
188
266
|
usuário ver o estado do robô no momento em que aquilo aconteceu (botão de imagem no app):
|
|
189
267
|
|
|
190
268
|
```js
|
|
@@ -199,16 +277,12 @@ await hobots.items.each(notas, async (nota, item) => {
|
|
|
199
277
|
item.occurrence('sem fatura no mês', undefined, { image: await page.screenshot() });
|
|
200
278
|
});
|
|
201
279
|
|
|
202
|
-
// na
|
|
203
|
-
|
|
204
|
-
try {
|
|
280
|
+
// na etapa
|
|
281
|
+
await hobots.step(`emitir ${numero}`, async etapa => {
|
|
205
282
|
await emitir(numero);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
} finally {
|
|
210
|
-
step.finish();
|
|
211
|
-
}
|
|
283
|
+
etapa.image(await page.screenshot()); // print do estado da tela
|
|
284
|
+
if (await page.isVisible('.aviso')) etapa.warning('portal demorou a responder', { image: await page.screenshot() });
|
|
285
|
+
}, { retries: 0 });
|
|
212
286
|
|
|
213
287
|
// numa issue (módulo Issues) — o print vai na linha de log que espelha o erro
|
|
214
288
|
hobots.captureException(erro, { attachment: hobots.image(await page.screenshot(), { caption: 'tela no momento do erro' }) });
|
|
@@ -220,7 +294,7 @@ O SDK não tira o print: aceita bytes (`Uint8Array`/`ArrayBuffer`/`Buffer`) ou u
|
|
|
220
294
|
|
|
221
295
|
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`:
|
|
222
296
|
|
|
223
|
-
- `hobots.items.*`, `hobots.progress.total/advance/set`, `hobots.
|
|
297
|
+
- `hobots.items.*`, `hobots.progress.total/advance/set`, `hobots.step()`, `hobots.sleep()` e cada tentativa de `hobots.retry()`.
|
|
224
298
|
- **Nunca `hobots.log.*`** — log em `finally` de limpeza não pode explodir.
|
|
225
299
|
|
|
226
300
|
```js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hobots",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
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,
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
"dist",
|
|
13
13
|
"docs"
|
|
14
14
|
],
|
|
15
|
+
"bin": {
|
|
16
|
+
"hobots": "./dist/cli.cjs"
|
|
17
|
+
},
|
|
15
18
|
"main": "./dist/index.cjs",
|
|
16
19
|
"module": "./dist/index.js",
|
|
17
20
|
"types": "./dist/index.d.ts",
|