hobots 2.4.0 → 3.2.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 +116 -367
- package/dist/index.cjs +1385 -909
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +252 -225
- package/dist/index.d.ts +252 -225
- package/dist/index.js +1385 -909
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +27 -2
- package/dist/protocol.d.ts +27 -2
- package/docs/README.md +16 -33
- package/docs/api-reference.md +75 -213
- package/docs/concepts.md +60 -393
- package/docs/getting-started.md +38 -84
- package/docs/protocol.md +31 -19
- package/docs/tasks.md +102 -247
- package/package.json +2 -2
- package/docs/telemetry.md +0 -162
package/docs/tasks.md
CHANGED
|
@@ -1,314 +1,169 @@
|
|
|
1
|
-
# Tasks
|
|
1
|
+
# Tasks
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Uma **task** é o que o robô sabe fazer, identificada por um slug. Cada rodada é uma **execução** (run) na plataforma, com logs, itens, progresso, anexos e resultado. Há dois modos de registrar uma task; tudo o mais (o objeto `hobots`, cancelamento, retry) é igual nos dois.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Ciclo de vida
|
|
5
|
+
## Os dois modos
|
|
8
6
|
|
|
9
7
|
```js
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
Hobots.init({
|
|
13
|
-
clientSecret: 'hb_<64 hex>',
|
|
14
|
-
instanceId: 'vm-cliente-01',
|
|
15
|
-
tasks: true, // ou { pollIntervalMs, concurrency, taskTimeoutMs, logFlushIntervalMs }
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
Hobots.register('processar-nfe', async (params, ctx) => {
|
|
19
|
-
ctx.log.info(`processando lote ${params.lote}…`);
|
|
20
|
-
ctx.throwIfCanceled(); // cancelamento cooperativo
|
|
21
|
-
ctx.log.info('12 notas processadas');
|
|
22
|
-
});
|
|
8
|
+
// sob demanda: uma execução por solicitação do app (formulário com este task_slug ou agendamento)
|
|
9
|
+
hobots.register('processar-nfe', async params => { /* … */ }, { concurrency: 2 });
|
|
23
10
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
process.on('SIGINT', () => {
|
|
27
|
-
void Hobots.close(15_000).then(() => process.exit(0));
|
|
28
|
-
});
|
|
11
|
+
// sempre ligado: o robô abre a própria execução e roda o handler (o seu loop) uma vez
|
|
12
|
+
hobots.start('monitor-pasta', async () => { /* while (true) { … } */ });
|
|
29
13
|
```
|
|
30
14
|
|
|
31
|
-
- `
|
|
32
|
-
- `
|
|
33
|
-
-
|
|
34
|
-
- `stop(timeoutMs?)` — para **só o polling de tasks**: encerra o polling e aguarda as runs ativas ([default](./api-reference.md#tempos-e-intervalos)); a telemetria continua. Retorna `false` se estourou o timeout.
|
|
35
|
-
- `close(timeoutMs?)` — encerra **tudo**: agente (aguardando runs em voo) → heartbeat → flush final.
|
|
15
|
+
- `register` — o robô faz polling na plataforma; cada solicitação vira uma execução com os `params` do formulário. Registre antes de haver solicitações; o polling começa sozinho.
|
|
16
|
+
- `start` — ao iniciar, o SDK cria a execução (`POST /v1/tasks/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
|
+
- Os dois coexistem no mesmo processo. Um slug só pode ser registrado uma vez (lança).
|
|
36
18
|
|
|
37
|
-
|
|
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 (até 10.000 itens).
|
|
38
20
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
## O handler e o `RunContext`
|
|
21
|
+
## O handler e o objeto `hobots`
|
|
42
22
|
|
|
43
23
|
```ts
|
|
44
|
-
type TaskHandler<P = Record<string, unknown>> =
|
|
45
|
-
(params: P, ctx: RunContext) => void | Promise<void>;
|
|
46
|
-
|
|
47
|
-
interface RunContext {
|
|
48
|
-
runId: string;
|
|
49
|
-
task: string;
|
|
50
|
-
log: RunLogger; // ctx.log.debug/info/warn/error/success(message)
|
|
51
|
-
items: RunItems; // ctx.items.succeeded/failed/occurrence — a entrega do bot
|
|
52
|
-
tx: Transaction;
|
|
53
|
-
signal: AbortSignal;
|
|
54
|
-
throwIfCanceled(): void;
|
|
55
|
-
sleep(ms: number): Promise<void>;
|
|
56
|
-
}
|
|
24
|
+
type TaskHandler<P = Record<string, unknown>> = (params: P, ctx: RunContext) => void | Promise<void>;
|
|
57
25
|
```
|
|
58
26
|
|
|
59
|
-
|
|
60
|
-
- **`ctx.log.<level>(message)`** — níveis `debug`, `info`, `warn`, `error` e `success`; logs enviados **incrementalmente** ao app (PATCHes com throttle), visíveis na solicitação em tempo quase real. Cada linha também é **impressa no terminal local** como `[hobots:<task>] INFO mensagem`, colorida por nível — útil para acompanhar o robô rodando na VM. A cor é desligada automaticamente fora de um TTY e quando a env `NO_COLOR` está definida; o eco local é independente do envio ao servidor.
|
|
61
|
-
- **`ctx.items`** — registro dos **itens que a execução processou/gerou** (notas fiscais baixadas, boletos emitidos…), com status e atributos. Aparecem na aba "itens" da solicitação, com totalizadores (veja a seção abaixo).
|
|
62
|
-
- **`ctx.tx`** — a [transaction de performance](./concepts.md#transaction-de-run-ctxtx) desta run, já correlacionada e finalizada pelo SDK. Use `ctx.tx.startChild()` para medir as etapas (veja abaixo).
|
|
63
|
-
- **`ctx.signal`** — `AbortSignal` que dispara quando a run é **cancelada no app**, em **timeout** ou no **shutdown**. Cancelamento é cooperativo (veja abaixo).
|
|
64
|
-
- **`ctx.throwIfCanceled()`** — lança `RunCanceled` se o `signal` já disparou. O ponto de parada de 1 linha para chamar entre as etapas do trabalho.
|
|
65
|
-
- **`ctx.sleep(ms)`** — sleep que respeita o `signal`: rejeita com `RunCanceled` no cancelamento em vez de esperar até o fim. Use no lugar do seu `sleep` em esperas longas.
|
|
66
|
-
- **Retorno** — **ignorado**. O SDK não captura nem envia o valor retornado pelo handler ao servidor — resultados de execução podem conter dados sensíveis e não são coletados.
|
|
67
|
-
- **Erro lançado** — a run é reportada como `failed` com tipo/mensagem/stack (mensagem cap 10k, stack 20k).
|
|
68
|
-
|
|
69
|
-
Você pode tipar os params: `Hobots.register<{ lote: number }>(...)`.
|
|
70
|
-
|
|
71
|
-
> **Dados sensíveis:** o que você escreve em `ctx.log` e as mensagens/stack de erros lançados **são enviados ao servidor** e ficam visíveis na execução no app. Não inclua senhas, tokens, documentos, dados pessoais ou qualquer informação sensível em logs e mensagens de erro.
|
|
72
|
-
|
|
73
|
-
## Itens de execução (`ctx.items`)
|
|
74
|
-
|
|
75
|
-
Logs contam **como** o bot trabalhou; itens contam **o que** ele entregou. Cada item é uma unidade de negócio processada pela execução — uma nota fiscal baixada, um boleto emitido — registrada com um dos três status:
|
|
76
|
-
|
|
77
|
-
- **`succeeded`** — processado com sucesso.
|
|
78
|
-
- **`failed`** — falha técnica ao processar o item (ex.: o download quebrou).
|
|
79
|
-
- **`occurrence`** — ocorrência de negócio, que não é falha do bot (ex.: "nota nº 10 não encontrada", "CNPJ do prestador incorreto").
|
|
27
|
+
Dentro do handler — e em **qualquer módulo** chamado por ele — o objeto global `hobots` resolve a execução atual:
|
|
80
28
|
|
|
81
29
|
```js
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
await baixar(nota);
|
|
90
|
-
ctx.items.succeeded({ numero_nota: nota.numero, cnpj_prestador: nota.cnpj }, { id: nota.numero });
|
|
91
|
-
} catch (erro) {
|
|
92
|
-
ctx.items.failed(String(erro), { numero_nota: nota.numero }, { id: nota.numero });
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Na plataforma, os itens aparecem na aba **itens** da solicitação, com totalizadores (total, sucesso, falhas, ocorrências), filtro por status e paginação. As chaves do `payload` que viram **colunas** da tabela são configuradas por formulário em Configurações → Formulários → itens da execução — o `payload` aceita qualquer atributo que você julgar útil (só as chaves configuradas são exibidas; o restante fica no registro).
|
|
99
|
-
|
|
100
|
-
### API
|
|
101
|
-
|
|
102
|
-
```ts
|
|
103
|
-
interface RunItems {
|
|
104
|
-
add(item: { status: 'succeeded' | 'failed' | 'occurrence'; id?: string; message?: string; payload?: Record<string, unknown> }): void;
|
|
105
|
-
succeeded(payload?, options?: { id?: string }): void;
|
|
106
|
-
failed(message, payload?, options?: { id?: string }): void;
|
|
107
|
-
occurrence(message, payload?, options?: { id?: string }): void;
|
|
30
|
+
// notas.js — não recebe ctx nenhum
|
|
31
|
+
export async function baixar(nota) {
|
|
32
|
+
const step = hobots.transaction.startChild('http.client', `GET /notas/${nota}`);
|
|
33
|
+
const xml = await portal.baixar(nota);
|
|
34
|
+
step.setData('bytes', xml.length).finish();
|
|
35
|
+
hobots.items.succeeded({ numero_nota: nota }, { id: nota });
|
|
108
36
|
}
|
|
109
37
|
```
|
|
110
38
|
|
|
111
|
-
|
|
112
|
-
- **`message`** — o motivo, esperado em `failed`/`occurrence` (cap 2.000 caracteres). Passa por `scrubSecrets`, como os logs.
|
|
113
|
-
- **`payload`** — atributos livres do item (cap 16 KB serializado; acima disso o payload é descartado com aviso e o item mantido). Strings passam por `scrubSecrets`.
|
|
114
|
-
- **Registro único** — o item chega com o status final; não existe "atualizar item". Se o bot reprocessa, use outra run (retry cria uma run nova, com itens próprios).
|
|
39
|
+
Isso funciona com `concurrency > 1` porque cada execução roda no seu contexto async (`AsyncLocalStorage`). Com uma única execução ativa, funciona até fora da cadeia async (um listener criado antes do handler, por exemplo). O `ctx` do 2º argumento é o mesmo objeto — use se preferir explícito. Tipar params: `hobots.register<{ lote: number }>(…)`.
|
|
115
40
|
|
|
116
|
-
|
|
41
|
+
| Membro | Notas |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `hobots.log.<debug\|info\|warn\|error\|success>(msg)` | Enviado incrementalmente (PATCHes com throttle) e ecoado no terminal. Nunca lança. |
|
|
44
|
+
| `hobots.items` | Ver [Itens](#itens-de-execução-hobotsitems) |
|
|
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. |
|
|
47
|
+
| `hobots.attach(imagem, opts?)` | Ver [Anexos](#anexos-screenshots) |
|
|
48
|
+
| `hobots.captureException(erro, hint?)` | Issue no módulo Issues; `hint.attachment` anexa um print à linha de log |
|
|
49
|
+
| `hobots.retry(fn, opts?)` | Ver [Retry](#retry) |
|
|
50
|
+
| `hobots.sleep(ms)` · `hobots.signal` · `hobots.throwIfCanceled()` | Ver [Cancelamento](#cancelamento-cooperativo) |
|
|
51
|
+
| `hobots.runId` · `hobots.task` · `hobots.params` | Identidade da execução |
|
|
117
52
|
|
|
118
|
-
|
|
53
|
+
- **Retorno** do handler: **ignorado** (resultados podem conter dados sensíveis). Reporte por logs e itens.
|
|
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.
|
|
119
55
|
|
|
120
|
-
|
|
56
|
+
> **Dados sensíveis:** logs, itens, erros e prints são enviados ao servidor e ficam visíveis no app.
|
|
121
57
|
|
|
122
|
-
|
|
58
|
+
## Itens de execução (`hobots.items`)
|
|
123
59
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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 na solicitação.
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
hobots.items.succeeded({ numero_nota: nota.numero, cnpj: nota.cnpj }, { id: nota.numero });
|
|
64
|
+
hobots.items.failed('portal fora do ar', { numero_nota: nota.numero }, { id: nota.numero }); // falha técnica
|
|
65
|
+
hobots.items.occurrence('nota não encontrada', { numero_nota: '10' }, { id: '10' }); // ocorrência de negócio
|
|
66
|
+
hobots.items.add({ status: 'succeeded', id: '11', payload: { … } }); // forma genérica
|
|
67
|
+
```
|
|
130
68
|
|
|
131
|
-
|
|
69
|
+
- Chamadas síncronas e bufferizadas (lotes de até 500); o SDK drena o buffer antes do reporte final.
|
|
70
|
+
- `id` é a identidade de negócio do item na execução — repetido, o servidor ignora (retry de rede e retry do handler idempotentes). Sem `id`, cada chamada é um item novo.
|
|
71
|
+
- Limites: 10.000 itens/execução, payload 16 KB, mensagem 2.000 caracteres. Uma execução cancelada ainda aceita os itens já bufferizados (trabalho feito).
|
|
72
|
+
- Cada chamada é um **ponto de parada cooperativo** (lança `RunCanceled` se a execução foi cancelada).
|
|
132
73
|
|
|
133
|
-
|
|
74
|
+
### Colunas da aba de itens
|
|
134
75
|
|
|
135
|
-
|
|
76
|
+
As colunas vêm do código, não do formulário — valem para `register()` (com ou sem formulário) e `start()`:
|
|
136
77
|
|
|
137
78
|
```js
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
ctx.progress.total(notas.length); // antes do loop
|
|
141
|
-
for (const nota of notas) {
|
|
142
|
-
await baixar(nota);
|
|
143
|
-
ctx.progress.advance(); // a cada item
|
|
144
|
-
}
|
|
79
|
+
register('processar-nfe', handler, {
|
|
80
|
+
items: { columns: ['numero_nota', { key: 'cnpj', label: 'CNPJ' }, { key: 'valor', label: 'valor (R$)' }] },
|
|
145
81
|
});
|
|
146
82
|
```
|
|
147
83
|
|
|
148
|
-
|
|
84
|
+
- Ordem da lista = ordem das colunas; até 12; chave com letras, números, `.`, `-`, `_` (inválidas são descartadas com aviso em `debug`).
|
|
85
|
+
- 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).
|
|
86
|
+
- Sem declaração nem personalização, o app mostra as chaves encontradas nos próprios payloads.
|
|
149
87
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
88
|
+
## Progresso (`hobots.progress`)
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
hobots.progress.total(notas.length);
|
|
92
|
+
for (const nota of notas) {
|
|
93
|
+
await baixar(nota);
|
|
94
|
+
hobots.progress.advance(); // barra, percentual e tempo restante no app
|
|
157
95
|
}
|
|
158
96
|
```
|
|
159
97
|
|
|
160
|
-
|
|
161
|
-
- **Independente de `ctx.items`** — use um, outro ou ambos; nenhum avança o outro (um item pode gerar vários registros, ou nenhum).
|
|
162
|
-
- **Total inválido** (`0`, negativo, não-inteiro) é ignorado com um aviso único no terminal.
|
|
163
|
-
- **Semântica de snapshot** — o servidor sobrescreve `current`/`total` com o último valor recebido; não há soma. Ajustar o `total` no meio da run (a lista cresceu) é permitido.
|
|
164
|
-
|
|
165
|
-
### Como funciona (e por que não gera tráfego)
|
|
166
|
-
|
|
167
|
-
As chamadas são **síncronas e baratas**: o snapshot mais recente pega carona em **qualquer** PATCH que saia — logs incrementais, heartbeat de cancelamento, reporte terminal. Só quando nada mais está a caminho o SDK dispara um PATCH `running` próprio, no máximo **1 por segundo**. `advance()` num loop de 10.000 itens em 5 segundos gera ~5 PATCHes, não 10.000. O PATCH terminal (`succeeded`/`failed`) **sempre** leva o valor final — uma run que falhou aparece como `87/100`.
|
|
168
|
-
|
|
169
|
-
O servidor grava `progress_started_at` no primeiro PATCH com progresso (relógio do servidor) — é a âncora da estimativa de término, calculada no app: `restante = decorrido × (total − current) ÷ current`.
|
|
98
|
+
Opcional e independente de `items`. Sem `total()` nada é enviado. Snapshot absoluto que pega carona nos PATCHes de log (no pior caso 1/s); o PATCH terminal leva o valor final. `set(atual, total?)`, `current` e `snapshot` completam a API. `total/advance/set` são pontos de parada cooperativos; `current`/`snapshot` nunca lançam.
|
|
170
99
|
|
|
171
100
|
## Anexos (screenshots)
|
|
172
101
|
|
|
173
|
-
Texto de log conta o que o robô fez; um print conta **como estava a tela**. Os métodos de captura — `Hobots.captureException(error, hint)` e `Hobots.captureMessage(message, level, hint)` — aceitam um **`attachment`** no hint. Dentro de uma execução de task, a imagem vira anexo da linha de log da run que espelha a captura, e aparece como miniatura logo abaixo dela na aba "logs do agente" da execução — clicável para ampliar.
|
|
174
|
-
|
|
175
102
|
```js
|
|
176
|
-
|
|
177
|
-
await entrar();
|
|
178
|
-
Hobots.captureMessage('portal após o login', 'info', {
|
|
179
|
-
attachment: await page.screenshot(),
|
|
180
|
-
});
|
|
103
|
+
await hobots.attach(await page.screenshot(), { caption: 'portal após o login' });
|
|
181
104
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
});
|
|
189
|
-
throw erro;
|
|
190
|
-
}
|
|
191
|
-
});
|
|
105
|
+
try {
|
|
106
|
+
await emitir(params);
|
|
107
|
+
} catch (erro) {
|
|
108
|
+
hobots.captureException(erro, { attachment: { image: await page.screenshot(), caption: 'tela no momento do erro' } });
|
|
109
|
+
throw erro;
|
|
110
|
+
}
|
|
192
111
|
```
|
|
193
112
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
### O que aceita
|
|
197
|
-
|
|
198
|
-
O `attachment` pode ser a imagem direto, ou um objeto com metadados:
|
|
199
|
-
|
|
200
|
-
| Entrada | Exemplo |
|
|
201
|
-
| --- | --- |
|
|
202
|
-
| bytes | `Uint8Array`, `ArrayBuffer` ou `Buffer` — `await page.screenshot()` |
|
|
203
|
-
| caminho de arquivo | `'C:\\prints\\tela.png'` (só em Node) |
|
|
204
|
-
| objeto | `{ image, caption?, contentType? }` |
|
|
205
|
-
|
|
206
|
-
| Campo do objeto | Tipo | Default | O que faz |
|
|
207
|
-
| --- | --- | --- | --- |
|
|
208
|
-
| `image` | bytes ou caminho | — | a imagem em si |
|
|
209
|
-
| `caption` | `string` | — | legenda curta exibida sob a imagem ampliada (máx. 200 caracteres) |
|
|
210
|
-
| `contentType` | `'image/png' \| 'image/jpeg' \| 'image/webp'` | detectado | só é usado quando os *magic bytes* não bastam |
|
|
211
|
-
|
|
212
|
-
O texto da linha de log é a própria mensagem capturada (`message` do `captureMessage`, ou `Nome: mensagem` do erro no `captureException`); o nível vem do `type` da captura (`'fatal'` vira `'error'` no log da run).
|
|
213
|
-
|
|
214
|
-
### Limites
|
|
215
|
-
|
|
216
|
-
| Limite | Valor |
|
|
217
|
-
| --- | --- |
|
|
218
|
-
| formatos | PNG, JPEG e WEBP |
|
|
219
|
-
| tamanho por imagem | 5 MB |
|
|
220
|
-
| anexos por execução | 20 |
|
|
221
|
-
| anexos por linha de log | 4 |
|
|
222
|
-
| uploads simultâneos | 3 (o resto fica na fila) |
|
|
223
|
-
|
|
224
|
-
### Como funciona (e por que não trava a run)
|
|
225
|
-
|
|
226
|
-
A linha de log é registrada **na hora**, com o id do anexo; o upload roda em segundo plano. O binário vai direto para o S3 por uma URL presignada — não passa pela API do Hobots. Enquanto o upload não termina, o app mostra a miniatura como "enviando imagem…".
|
|
227
|
-
|
|
228
|
-
O anexo **nunca faz a captura lançar**. Todo insucesso vira um aviso no log da run:
|
|
229
|
-
|
|
230
|
-
| Motivo | Quando |
|
|
231
|
-
| --- | --- |
|
|
232
|
-
| `too-large` | acima de 5 MB |
|
|
233
|
-
| `quota` | passou de 20 anexos na execução |
|
|
234
|
-
| `unsupported-format` | não é PNG/JPEG/WEBP |
|
|
235
|
-
| `read-failed` | o caminho de arquivo não pôde ser lido |
|
|
236
|
-
| `unsupported` | a instalação do Hobots não tem anexos habilitados |
|
|
237
|
-
| `upload-failed` | falha de rede depois dos retries |
|
|
238
|
-
| `canceled` | a run foi cancelada ou encerrada durante o upload |
|
|
239
|
-
|
|
240
|
-
O upload é sempre fire-and-forget — a captura retorna na hora. Antes de reportar o desfecho da run, o SDK aguarda os uploads pendentes por até 15 s, para a execução não terminar com miniaturas em "enviando…".
|
|
113
|
+
O SDK não tira o print: aceita bytes (`Uint8Array`/`ArrayBuffer`/`Buffer`) ou um caminho de arquivo. A linha de log entra na hora; 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 linha de log. Fora de uma execução não há onde pendurar a imagem (descartada com aviso).
|
|
241
114
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
> **Dados sensíveis:** o print é enviado ao servidor **exatamente como está** — o SDK não borra nem recorta nada. Uma tela de portal costuma conter CPF, nome, saldo, número de documento. Capture apenas o necessário e evite prints de telas com dados de terceiros. Solicitantes (papel `requester`) só veem os anexos quando o projeto está com "mostrar logs para solicitantes" ligado, o mesmo gate dos logs.
|
|
115
|
+
## Cancelamento cooperativo
|
|
245
116
|
|
|
246
|
-
|
|
117
|
+
Cancelar a solicitação 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`:
|
|
247
118
|
|
|
248
|
-
|
|
119
|
+
- `hobots.items.*`, `hobots.progress.total/advance/set`, `hobots.transaction.startChild()`, `hobots.sleep()` e cada tentativa de `hobots.retry()`.
|
|
120
|
+
- **Nunca `hobots.log.*`** — log em `finally` de limpeza não pode explodir.
|
|
249
121
|
|
|
250
122
|
```js
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
123
|
+
hobots.register('processar-nfe', async params => {
|
|
124
|
+
const browser = await abrir();
|
|
125
|
+
try {
|
|
126
|
+
for (const nota of params.notas) {
|
|
127
|
+
await processar(nota);
|
|
128
|
+
hobots.items.succeeded({ numero_nota: nota }, { id: nota }); // para aqui se cancelada
|
|
129
|
+
}
|
|
130
|
+
} finally {
|
|
131
|
+
hobots.log.info('fechando navegador'); // seguro: log nunca lança
|
|
132
|
+
await browser.close();
|
|
261
133
|
}
|
|
262
134
|
});
|
|
263
135
|
```
|
|
264
136
|
|
|
265
|
-
|
|
137
|
+
`hobots.throwIfCanceled()` e `hobots.signal` (um `AbortSignal`, útil para passar a `fetch`) ficam para controle fino. O `RunCanceled` de um cancelamento vindo do app é suprimido — nada é reportado por cima. Se o handler ignorar o signal por `cancelGraceMs` (10 s), o SDK libera o slot e o handler segue em segundo plano, com todos os reportes descartados.
|
|
266
138
|
|
|
267
|
-
|
|
268
|
-
- `ctx.tx` **não respeita o `tracesSampleRate`** — toda run gera uma transaction. A amostragem vale só para as transactions abertas com `startTransaction()` fora de uma run.
|
|
269
|
-
- Step que você esquecer aberta é fechada no `finish()` da transaction, então o dado não se perde.
|
|
270
|
-
- Marque etapas com ressalva usando `step.setStatus('warning')` (ex.: retry que deu certo, resultado parcial): elas aparecem em âmbar no waterfall, distintas de sucesso e de falha (`'error'`).
|
|
139
|
+
> Não use `if (hobots.signal.aborted) return;` — retornar reporta `succeeded`. É preciso lançar.
|
|
271
140
|
|
|
272
|
-
##
|
|
141
|
+
## Retry
|
|
273
142
|
|
|
274
|
-
|
|
143
|
+
O handler que lança é **re-invocado na mesma execução**: aviso no log da execução (`tentativa 1/2 falhou: <erro> — nova tentativa em 5000ms`), espera cancelável, mesmos `params`. Default `retries: 1` (2 tentativas), `retryDelayMs: 5_000` (ms ou `(tentativa) => ms`). Esgotadas → `failed` com o último erro.
|
|
144
|
+
|
|
145
|
+
Nunca há retry em `RunCanceled`, `TaskTimeout` (o timeout é por tentativa e terminal) ou execução já cancelada no app. Itens e progresso já enviados permanecem — o handler precisa ser **idempotente** (use o `id` de negócio nos itens).
|
|
146
|
+
|
|
147
|
+
Para uma chamada instável dentro do handler:
|
|
275
148
|
|
|
276
149
|
```js
|
|
277
|
-
|
|
278
|
-
for (const nota of params.notas) {
|
|
279
|
-
ctx.throwIfCanceled(); // lança RunCanceled se cancelada — o handler para aqui
|
|
280
|
-
await processar(nota);
|
|
281
|
-
}
|
|
282
|
-
});
|
|
150
|
+
const dados = await hobots.retry(() => portal.buscar(nota), { retries: 3, delayMs: attempt => attempt * 500, shouldRetry: e => e.code !== 'ENOTFOUND' });
|
|
283
151
|
```
|
|
284
152
|
|
|
285
|
-
|
|
153
|
+
`hobots.retry` usa o signal da execução: cancelada, ele lança `RunCanceled` em vez de insistir. Fora de uma execução funciona igual, sem cancelamento.
|
|
286
154
|
|
|
287
|
-
|
|
155
|
+
## Concorrência (`register`)
|
|
288
156
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
Se o processo do agente **morre no meio de uma run**, a solicitação vira **falha** no app com erro `AgentTerminated` ("execução encerrada inesperadamente") — sem re-execução automática. Dois caminhos garantem isso:
|
|
157
|
+
`concurrency` (1..5, default 1) por task. O agente só pede à plataforma o que consegue rodar (tasks com slot livre, `max` = soma dos slots, teto 5) e entra em **drain mode** — re-pesquisa imediatamente enquanto vem trabalho; sem trabalho, espera `pollIntervalMs`.
|
|
292
158
|
|
|
293
|
-
|
|
294
|
-
- **Morte dura** (kill -9, queda de energia, rede fora): nada roda no cliente. O heartbeat de cancelamento é também o **sinal de vida da run** — o servidor marca a run como falha ao notar silêncio prolongado (o [sweep](./api-reference.md#tempos-e-intervalos) server-side).
|
|
159
|
+
## Timeout
|
|
295
160
|
|
|
296
|
-
`
|
|
161
|
+
`timeoutMs` (default: nenhum) é por tentativa e **client-side**: no estouro o SDK dispara o signal, libera o slot e reporta `failed`/`TaskTimeout` — sem nova tentativa. O handler não é morto; respeite o signal para parar de verdade.
|
|
297
162
|
|
|
298
163
|
## Idempotência (at-least-once até `running`)
|
|
299
164
|
|
|
300
|
-
Se o agente morrer logo
|
|
301
|
-
|
|
302
|
-
## Concorrência
|
|
303
|
-
|
|
304
|
-
`concurrency` (default 1, máx 5) controla quantas runs o agente executa em paralelo. Com 1, as runs são sequenciais. O agente só reivindica o que consegue rodar (`max = concurrency - runs ativas`) e entra em **[drain mode](./concepts.md#drain-mode)**: enquanto vêm runs, re-pesquisa imediatamente; quando não vem nada, espera `pollIntervalMs`. Ou seja, o intervalo de poll é o teto da espera **quando não há trabalho** — não a latência típica para começar uma run.
|
|
165
|
+
Se o agente morrer logo após capturar uma execução (antes de reportar `running`), ela é re-entregue quando um agente voltar (até 3 tentativas; depois `AgentLost`). A partir de `running`, a morte do agente **não** re-entrega — a execução vira `AgentTerminated`, porque o trabalho pode ter efeitos parciais. Somado ao retry do handler, a regra é uma: processar a mesma coisa duas vezes não pode duplicar efeito.
|
|
305
166
|
|
|
306
|
-
##
|
|
307
|
-
|
|
308
|
-
`taskTimeoutMs` é um timeout **client-side**. No estouro o SDK **não mata** o handler — ele dispara `ctx.signal`, libera o slot de concorrência e reporta a run como falha (`TaskTimeout`). O handler pode continuar rodando em background; respeite o `signal` para parar de verdade. Sem `taskTimeoutMs`, o backstop é o servidor.
|
|
309
|
-
|
|
310
|
-
Todas as opções em [Referência de API → TasksOptions](./api-reference.md#tasksoptions).
|
|
311
|
-
|
|
312
|
-
## Como o claim funciona (resumo)
|
|
167
|
+
## Encerramento inesperado
|
|
313
168
|
|
|
314
|
-
|
|
169
|
+
Sem handler próprio de `SIGINT`/`SIGTERM` (ou em erro fatal), o SDK reporta as execuções ativas como `failed`/`AgentTerminated` antes de sair. Com handler próprio, chame `hobots.close()` nele. Se o processo morrer sem reportar, o servidor marca a execução após ~120 s sem sinal de vida.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hobots",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "SDK JavaScript da Hobots —
|
|
3
|
+
"version": "3.2.0",
|
|
4
|
+
"description": "SDK JavaScript da Hobots — tudo é uma task: register() atende solicitações da plataforma, start() roda sempre 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",
|