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/README.md
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# hobots
|
|
2
2
|
|
|
3
|
-
SDK JavaScript/TypeScript da **Hobots** para
|
|
3
|
+
SDK JavaScript/TypeScript da **Hobots** para robôs (RPAs) e integrações. **Tudo é uma task**: você registra o que o robô faz e a plataforma passa a receber logs, itens processados, progresso, prints, erros e o "estou vivo" do robô.
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
|
|
8
|
-
**Um único import e um único `init()`** cobrem as duas capabilities. Cliente HTTP puro, **sem dependências de runtime**. Requer **Node 18+** (usa o `fetch` global); a telemetria também roda no browser para captura de erros.
|
|
5
|
+
- **`hobots.register(task, handler)`** — **sob demanda**: o robô fica online aguardando **solicitações** criadas no app (formulários e agendamentos) e executa o handler a cada uma. Módulo **Solicitações**.
|
|
6
|
+
- **`hobots.start(task, handler)`** — **sempre ligado**: o robô abre a própria execução ao iniciar e roda o handler (o seu loop) dentro dela; a plataforma só recebe logs, itens e progresso. Sem formulário, sem solicitante.
|
|
9
7
|
|
|
10
8
|
---
|
|
11
9
|
|
|
@@ -17,430 +15,181 @@ npm install hobots
|
|
|
17
15
|
```
|
|
18
16
|
|
|
19
17
|
```js
|
|
20
|
-
import
|
|
18
|
+
import hobots from 'hobots'; // ou: import { hobots, register, start } from 'hobots'
|
|
21
19
|
```
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
CommonJS: `const { hobots } = require('hobots')`.
|
|
24
22
|
|
|
25
23
|
---
|
|
26
24
|
|
|
27
|
-
##
|
|
28
|
-
|
|
29
|
-
O SDK expõe **duas capabilities** autenticadas pelo **mesmo Client Secret do agente**, configuradas em um único `init()`:
|
|
30
|
-
|
|
31
|
-
| Capability | Como habilita | Para quê | Ciclo de vida |
|
|
32
|
-
| --- | --- | --- | --- |
|
|
33
|
-
| **Telemetria** | sempre ativa no `init()` | O que o programa emite sozinho: erros, transactions/etapas, heartbeat. | Timers `unref`'d — **não** segura o processo. Chame `close()` no fim. |
|
|
34
|
-
| **Tasks** | `tasks: true` (ou opções) no `init()` | Execução remota sob demanda via polling. | `start()` **segura o processo vivo** até `stop()`/`close()`. |
|
|
35
|
-
|
|
36
|
-
Você usa **uma, outra ou as duas**:
|
|
37
|
-
|
|
38
|
-
- Só saber se o robô quebrou, quanto demora e se está vivo? → `init({ clientSecret, heartbeat: true })`.
|
|
39
|
-
- Disparar o robô sob demanda a partir de um formulário no app? → `init({ clientSecret, tasks: true })` + `register()` + `start()`.
|
|
40
|
-
- Ambos? Passe `heartbeat` e `tasks` no mesmo `init()` — o `instanceId` é a identidade única, e os dois canais convergem em **um agente** no painel. Com `tasks: true`, o heartbeat é **opcional**: o próprio poll de tasks já marca a presença do agente.
|
|
25
|
+
## Em 30 segundos
|
|
41
26
|
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
heartbeat: true, // telemetria: "estou vivo" a cada 30s
|
|
47
|
-
tasks: { pollIntervalMs: 2_000 }, // execução sob demanda
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
Hobots.register('processar-nfe', async (params, ctx) => {
|
|
51
|
-
ctx.log.info('começando…');
|
|
52
|
-
ctx.log.info('12 notas processadas'); // o retorno do handler é ignorado — reporte por log
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
Hobots.start(); // inicia o polling de tasks (segura o processo vivo)
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
## O Client Secret
|
|
61
|
-
|
|
62
|
-
Cada agente tem **um único Client Secret** — a mesma credencial autentica a telemetria **e** as tasks:
|
|
63
|
-
|
|
64
|
-
```
|
|
65
|
-
hb_<64 hex>
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
A credencial é **secreta** e viaja sempre no header `Authorization: Bearer` — nunca na URL. Copie o Client Secret no app, em **Configurações → Agentes** — os snippets de integração já vêm prontos. O SDK fala com a API oficial `https://api2.hobots.app` automaticamente; em desenvolvimento, aponte para outro host com a env `HOBOTS_API_URL` (ex.: `HOBOTS_API_URL=http://localhost:3001`).
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
## Telemetria
|
|
73
|
-
|
|
74
|
-
Todas as funções abaixo são **no-op seguras** se `Hobots.init()` não foi chamado (com aviso no console).
|
|
75
|
-
|
|
76
|
-
### `init(options)`
|
|
77
|
-
|
|
78
|
-
Inicialize **uma vez**, o mais cedo possível no processo. Cria o cliente, instala handlers globais de erro (a menos que `captureUnhandled: false`) e inicia o heartbeat se configurado.
|
|
79
|
-
|
|
80
|
-
```js
|
|
81
|
-
Hobots.init({
|
|
82
|
-
clientSecret: 'hb_<64 hex>',
|
|
83
|
-
environment: 'production',
|
|
84
|
-
release: 'robo-nfe@1.4.2',
|
|
85
|
-
instanceId: 'vm-cliente-01',
|
|
86
|
-
tags: { squad: 'automacoes' },
|
|
87
|
-
heartbeat: true, // "estou vivo" a cada 30s
|
|
88
|
-
});
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
| Opção | Tipo | Default | Descrição |
|
|
92
|
-
| --- | --- | --- | --- |
|
|
93
|
-
| `clientSecret` | `string` | **obrigatório** | o Client Secret do agente (`hb_<64 hex>`), copiado em **Configurações → Agentes** |
|
|
94
|
-
| `environment` | `string` | `'production'` | ambiente lógico |
|
|
95
|
-
| `release` | `string` | — | versão do programa, ex. `robo-nfe@1.4.2` |
|
|
96
|
-
| `instanceId` | `string` | **obrigatório** | identidade única da instância — heartbeat e poll de tasks convergem nela |
|
|
97
|
-
| `tags` | `Record<string,string>` | — | tags aplicadas a todos os eventos |
|
|
98
|
-
| `sampleRate` | `number` | `1` | amostragem de erros (0..1) |
|
|
99
|
-
| `tracesSampleRate` | `number` | `1` | amostragem de transactions (0..1) |
|
|
100
|
-
| `maxBreadcrumbs` | `number` | `100` | tamanho do buffer de breadcrumbs |
|
|
101
|
-
| `captureUnhandled` | `boolean` | `true` | handlers globais de erro (`uncaughtException`/`unhandledRejection`) |
|
|
102
|
-
| `machineMetrics` | `boolean` | `true` | CPU/memória/disco no heartbeat, no poll e em `contexts.machine` dos eventos |
|
|
103
|
-
| `heartbeat` | `boolean \| HeartbeatOptions` | `false` | heartbeat periódico (`true` usa defaults) |
|
|
104
|
-
| `tasks` | `boolean \| TasksOptions` | `false` | habilita a execução sob demanda (`true` usa defaults) — veja [Tasks](#tasks-execução-sob-demanda) |
|
|
105
|
-
| `beforeSend` | `(event) => event \| null` | — | edita/descarta evento (retorne `null` para descartar) |
|
|
106
|
-
| `debug` | `boolean` | `false` | loga atividade do SDK |
|
|
107
|
-
| `flushIntervalMs` | `number` | `2000` | intervalo do flush automático do transport |
|
|
108
|
-
| `maxBatchSize` | `number` | `10` | itens por envelope antes do flush imediato |
|
|
109
|
-
|
|
110
|
-
### Captura de erros e mensagens
|
|
111
|
-
|
|
112
|
-
```js
|
|
113
|
-
try {
|
|
114
|
-
await processarNota(nota);
|
|
115
|
-
} catch (error) {
|
|
116
|
-
Hobots.captureException(error, {
|
|
117
|
-
type: 'error',
|
|
118
|
-
tags: { cliente: 'acme' },
|
|
119
|
-
fingerprint: ['sefaz-timeout'], // agrupa manualmente; sem isso o servidor agrupa pelo stack
|
|
120
|
-
});
|
|
121
|
-
throw error;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
Hobots.captureMessage('Lote processado com sucesso', 'success');
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Ambas retornam o `event_id` (ou `undefined` se não inicializado). O hint aceita `type`, `tags`, `extra` e `fingerprint`. Erros **não tratados** são capturados e flushados automaticamente antes do processo sair — desligue com `captureUnhandled: false`. Os eventos aparecem agrupados em issues no módulo **Issues** do app.
|
|
128
|
-
|
|
129
|
-
`EventType = 'fatal' | 'error' | 'warning' | 'success'`. Não há `'info'`/`'debug'`: narração não abre issue — para isso existem o log da execução e as breadcrumbs. Use `'success'` para reportar itens concluídos com êxito — contam como eventos, mas **não** viram issue nem disparam alertas.
|
|
130
|
-
|
|
131
|
-
### Breadcrumbs
|
|
132
|
-
|
|
133
|
-
Trilha do que aconteceu antes do erro; enviada junto do próximo evento (buffer limitado por `maxBreadcrumbs`, mais antigos descartados).
|
|
134
|
-
|
|
135
|
-
```js
|
|
136
|
-
Hobots.addBreadcrumb({ category: 'nfe', message: 'Baixou XML da nota 123', level: 'info' });
|
|
27
|
+
```bash
|
|
28
|
+
export HOBOTS_CLIENT_SECRET=hb_<64 hex> # Configurações → Agentes
|
|
29
|
+
export HOBOTS_INSTANCE_ID=vm-cliente-01 # opcional — default: hostname da máquina
|
|
30
|
+
export HOBOTS_ENVIRONMENT=staging # opcional — default: production
|
|
137
31
|
```
|
|
138
32
|
|
|
139
|
-
A lista **não** é limpa ao enviar um evento e é global. Conceito: [Breadcrumb](./docs/concepts.md#breadcrumb).
|
|
140
|
-
|
|
141
|
-
### Escopo: tags, usuário, contexto, extras
|
|
142
|
-
|
|
143
|
-
Dados anexados a **todos** os eventos seguintes:
|
|
144
|
-
|
|
145
33
|
```js
|
|
146
|
-
|
|
147
|
-
Hobots.setTags({ squad: 'automacoes', cliente: 'acme' });
|
|
148
|
-
Hobots.setUser({ id: 'op-42', email: 'op@acme.com' });
|
|
149
|
-
Hobots.setContext('vm', { cpu: 4, ram: '8GB' });
|
|
150
|
-
Hobots.setExtra('ultimoLote', 128);
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
Para um escopo **isolado** (sem vazar para o global), use `withScope`:
|
|
34
|
+
import hobots from 'hobots';
|
|
154
35
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
36
|
+
// sob demanda: uma execução por solicitação do app (o task_slug casa com o formulário)
|
|
37
|
+
hobots.register('processar-nfe', async params => {
|
|
38
|
+
hobots.log.info(`lote ${params.lote}`);
|
|
39
|
+
hobots.progress.total(params.notas.length);
|
|
40
|
+
for (const nota of params.notas) {
|
|
41
|
+
await baixar(nota);
|
|
42
|
+
hobots.items.succeeded({ numero_nota: nota }, { id: nota });
|
|
43
|
+
hobots.progress.advance();
|
|
44
|
+
}
|
|
159
45
|
});
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
### Performance: transactions e etapas
|
|
163
|
-
|
|
164
|
-
```js
|
|
165
|
-
const tx = Hobots.startTransaction('processar-lote', 'rpa.job');
|
|
166
|
-
|
|
167
|
-
const step = tx.startChild('http.client', 'GET /api/notas');
|
|
168
|
-
await api.buscarNotas();
|
|
169
|
-
step.setData('notas', 42).finish();
|
|
170
|
-
|
|
171
|
-
const filho = step.startChild('db.query', 'INSERT INTO notas'); // etapas aninham
|
|
172
|
-
await gravar();
|
|
173
|
-
filho.finish();
|
|
174
|
-
|
|
175
|
-
tx.setStatus('ok'); // ou 'error'
|
|
176
|
-
tx.finish(); // só aqui a transaction é enviada (respeitando tracesSampleRate)
|
|
177
|
-
```
|
|
178
46
|
|
|
179
|
-
|
|
180
|
-
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
Bate "estou vivo" periodicamente; o servidor acusa o robô **offline** quando os batimentos param.
|
|
188
|
-
|
|
189
|
-
```js
|
|
190
|
-
Hobots.init({
|
|
191
|
-
clientSecret,
|
|
192
|
-
heartbeat: { intervalMs: 30_000, name: 'Robô NFe — VM 01' },
|
|
47
|
+
// sempre ligado: o handler é o loop do robô; cada iteração reporta o que fez
|
|
48
|
+
hobots.start('monitor-pasta', async () => {
|
|
49
|
+
while (true) {
|
|
50
|
+
await hobots.sleep(60_000); // cancelável
|
|
51
|
+
for (const arquivo of await novosArquivos()) hobots.items.succeeded({ arquivo });
|
|
52
|
+
hobots.log.info('ciclo ok');
|
|
53
|
+
}
|
|
193
54
|
});
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
`heartbeat: true` usa os defaults. A identidade é o `instanceId` do `init()`; os campos são `intervalMs`, `name` (default: o `instanceId`) e `metadata` — valores em [Tempos e intervalos](./docs/api-reference.md#tempos-e-intervalos). O primeiro batimento sai **na hora** em que o `init()` roda, não depois do primeiro intervalo.
|
|
197
55
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
### Recursos da máquina (CPU, memória, disco)
|
|
201
|
-
|
|
202
|
-
Ligado por padrão em Node (`machineMetrics: true`), o SDK coleta CPU do sistema, memória, disco do volume do bot e RSS/CPU do próprio processo — **sem dependência nova** e sem timer próprio: a leitura pega carona em cada heartbeat e em cada poll de tasks (série da instância em **Instâncias → recursos da máquina**) e vai como `contexts.machine` em cada evento (card **"máquina no momento do erro"** no detalhe da issue). Assim dá para responder se o ambiente tem recurso suficiente e se um erro coincidiu com CPU/memória no limite.
|
|
203
|
-
|
|
204
|
-
```js
|
|
205
|
-
Hobots.init({ clientSecret, instanceId: 'vm-01', heartbeat: true }); // coleta ligada
|
|
206
|
-
Hobots.init({ clientSecret, instanceId: 'vm-01', machineMetrics: false }); // desliga
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
O snapshot tem ~600 bytes (teto do servidor: 4 KiB). Formato em [Protocolo → MachineSnapshot](./docs/protocol.md#machinesnapshot).
|
|
210
|
-
|
|
211
|
-
### Flush e close
|
|
212
|
-
|
|
213
|
-
```js
|
|
214
|
-
await Hobots.flush(2000); // envia o buffer, mantém o SDK vivo
|
|
215
|
-
await Hobots.close(2000); // flush final + para o heartbeat + fecha o transport
|
|
56
|
+
process.on('SIGINT', () => void hobots.close(15_000).then(() => process.exit(0)));
|
|
216
57
|
```
|
|
217
58
|
|
|
218
|
-
|
|
59
|
+
O objeto **`hobots`** é global: `hobots.log`, `hobots.items`, `hobots.progress`, `hobots.transaction`… resolvem a execução **atual** de qualquer módulo do programa — sem passar contexto de mão em mão, mesmo com várias execuções em paralelo. Depois da primeira task ele também existe como `globalThis.hobots`.
|
|
219
60
|
|
|
220
61
|
---
|
|
221
62
|
|
|
222
|
-
##
|
|
223
|
-
|
|
224
|
-
Capability de **execução remota sob demanda**, habilitada com `tasks: true` (ou opções) no `init()`. O agente faz polling no app, executa os handlers registrados quando aparece trabalho e reporta status e logs incrementais — cada execução aparece no módulo **Solicitações** do app. Mesmo Client Secret, mesmo `debug` e o `instanceId` como identidade — nenhum handler global extra é instalado.
|
|
225
|
-
|
|
226
|
-
### Ciclo de vida
|
|
227
|
-
|
|
228
|
-
```js
|
|
229
|
-
Hobots.init({
|
|
230
|
-
clientSecret: 'hb_<64 hex>',
|
|
231
|
-
instanceId: 'vm-cliente-01',
|
|
232
|
-
tasks: true, // ou { pollIntervalMs, concurrency, ... }
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
Hobots.register('processar-nfe', async (params, ctx) => {
|
|
236
|
-
ctx.log.info(`processando lote ${params.lote}…`);
|
|
237
|
-
ctx.throwIfCanceled(); // cancelamento cooperativo
|
|
238
|
-
ctx.log.info('12 notas processadas');
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
Hobots.start(); // começa o polling e MANTÉM O PROCESSO VIVO
|
|
242
|
-
|
|
243
|
-
process.on('SIGINT', () => {
|
|
244
|
-
void Hobots.close(15_000).then(() => process.exit(0));
|
|
245
|
-
});
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
- `init({ tasks })` — cria o agente. Chamar `init()` de novo para o agente anterior e o substitui (o registry de handlers é preservado).
|
|
249
|
-
- `register(task, handler)` — `task` é o `task_slug` configurado no formulário do app (**Configurações → Formulários**). Registre **antes** de `start()`. Funciona até antes do `init()`; com as tasks desabilitadas, avisa no console.
|
|
250
|
-
- `start()` — começa o polling. Lança se o `init()` não configurou `tasks` ou se nenhuma task foi registrada.
|
|
251
|
-
- `stop(timeoutMs?)` — para **só o polling de tasks**: encerra o polling e aguarda as runs ativas (default 30s); a telemetria continua. Retorna `false` se estourou o timeout.
|
|
252
|
-
- `close(timeoutMs?)` — encerra **tudo**: agente (aguardando runs em voo) → heartbeat → flush final do transport.
|
|
253
|
-
|
|
254
|
-
| Opção (`TasksOptions`) | Tipo | Default | Descrição |
|
|
255
|
-
| --- | --- | --- | --- |
|
|
256
|
-
| `pollIntervalMs` | `number` | `2000` | intervalo entre polls sem trabalho (mínimo `1000`) |
|
|
257
|
-
| `concurrency` | `number` | `1` | runs simultâneas (clamp 1..5) |
|
|
258
|
-
| `taskTimeoutMs` | `number` | — | timeout client-side por run (não mata o handler; servidor é o backstop) |
|
|
259
|
-
| `logFlushIntervalMs` | `number` | `2000` | intervalo do envio incremental de logs |
|
|
260
|
-
| `cancelCheckIntervalMs` | `number` | `5000` | heartbeat de cancelamento por run (clamp 1000..15000) — também é o sinal de vida da run |
|
|
261
|
-
| `installExitHandlers` | `boolean` | `true` | handlers de SIGINT/SIGTERM/uncaughtException que reportam as runs ativas como `AgentTerminated` antes do processo morrer; nunca interferem quando o programa tem handler próprio |
|
|
262
|
-
|
|
263
|
-
O Client Secret, o `debug` e a identidade do agente (`instanceId`) vêm do `init()` — o heartbeat e o poll de tasks convergem em **um agente** no painel automaticamente.
|
|
63
|
+
## Credenciais e identidade
|
|
264
64
|
|
|
265
|
-
|
|
65
|
+
| Env | Opção | O que é |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| `HOBOTS_CLIENT_SECRET` | `clientSecret` | O **Client Secret do agente** (`hb_<64 hex>`), copiado em **Configurações → Agentes**. Viaja só no header `Authorization: Bearer`, nunca na URL. Ausente ou inválido → `register()`/`start()` lança na hora, citando a env (sem ecoar o valor). |
|
|
68
|
+
| `HOBOTS_INSTANCE_ID` | `instanceId` | Identidade **estável** desta instância do robô (`vm-cliente-01`). Sem ela, o hostname. Heartbeat e tasks convergem nela: **um agente só** no painel. |
|
|
69
|
+
| `HOBOTS_ENVIRONMENT` | `environment` | Ambiente lógico do robô (`development` \| `testing` \| `staging` \| `production`). Sem ela, `production`. |
|
|
70
|
+
| `HOBOTS_API_URL` | — | Só em desenvolvimento: aponta o SDK para outro host (`http://localhost:3001`). Em produção é sempre `https://api2.hobots.app`. |
|
|
266
71
|
|
|
267
|
-
|
|
268
|
-
type TaskHandler<P = Record<string, unknown>> =
|
|
269
|
-
(params: P, ctx: RunContext) => void | Promise<void>;
|
|
270
|
-
|
|
271
|
-
interface RunContext {
|
|
272
|
-
runId: string;
|
|
273
|
-
task: string;
|
|
274
|
-
log: RunLogger; // ctx.log.debug/info/warn/error/success(message)
|
|
275
|
-
items: RunItems; // ctx.items.succeeded/failed/occurrence — a entrega do bot
|
|
276
|
-
progress: RunProgressReporter; // ctx.progress.total/advance/set — barra + tempo restante no app
|
|
277
|
-
tx: Transaction; // transaction desta run — correlacionada e finalizada pelo SDK
|
|
278
|
-
signal: AbortSignal;
|
|
279
|
-
throwIfCanceled(): void; // lança RunCanceled se o signal já disparou
|
|
280
|
-
sleep(ms: number): Promise<void>; // espera cancelável — rejeita com RunCanceled no abort
|
|
281
|
-
}
|
|
282
|
-
```
|
|
72
|
+
A opção passada em `register()`/`start()` tem precedência sobre a env — útil para copiar e colar o snippet do app.
|
|
283
73
|
|
|
284
|
-
|
|
285
|
-
- **`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 quase em tempo real; também impressos no terminal local, coloridos por nível.
|
|
286
|
-
- **`ctx.items`** — registra os **itens que a execução processou/gerou** (a entrega do bot: notas fiscais baixadas, boletos emitidos…), cada um com status `succeeded`, `failed` (falha técnica) ou `occurrence` (ocorrência de negócio, ex.: "nota nº 10 não encontrada") e um `payload` de atributos livres. Aparecem na aba **itens** da solicitação, com totalizadores; as colunas exibidas são configuradas por formulário no app. Guia: [Itens de execução](./docs/tasks.md#itens-de-execução-ctxitems).
|
|
287
|
-
- **`ctx.progress`** — **opcional**: declare quantos itens a run vai processar (`ctx.progress.total(n)`) e avance a cada item (`ctx.progress.advance()`); o app mostra barra, percentual e tempo restante estimado na solicitação. Independente de `ctx.items`. Guia: [Progresso](./docs/tasks.md#progresso-da-execução-ctxprogress).
|
|
288
|
-
- **`ctx.tx`** — a transaction de performance desta run, já correlacionada e finalizada pelo SDK. Use `ctx.tx.startChild(op, description)` para medir as etapas — elas viram o waterfall da execução no app. `startTransaction()` chamado dentro do handler retorna essa mesma transaction. Conceito: [ctx.tx](./docs/concepts.md#transaction-de-run-ctxtx).
|
|
289
|
-
- **`ctx.signal`** — `AbortSignal` que dispara quando a run é **cancelada no app**, em **timeout** ou no **shutdown**.
|
|
290
|
-
- **`ctx.throwIfCanceled()`** — o ponto de parada de uma linha; lança `RunCanceled` se o `signal` já disparou.
|
|
291
|
-
- **`ctx.sleep(ms)`** — espera que respeita o `signal` (rejeita com `RunCanceled` em vez de dormir até o fim).
|
|
292
|
-
- **Retorno** — **ignorado**: o SDK não captura nem envia o valor retornado pelo handler (resultados podem conter dados sensíveis e não são coletados).
|
|
293
|
-
- **Erro lançado** — a run é reportada como `failed` (mensagem cap 10k, stack 20k).
|
|
294
|
-
|
|
295
|
-
> **Dados sensíveis:** `ctx.log` e mensagens/stack de erros **são enviados ao servidor** e ficam visíveis no app. Não inclua senhas, tokens, dados pessoais ou qualquer informação sensível em logs e mensagens de erro.
|
|
74
|
+
---
|
|
296
75
|
|
|
297
|
-
|
|
76
|
+
## Os dois modos
|
|
298
77
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
ctx.items.failed(String(erro), { numero_nota: nota.numero }, { id: nota.numero });
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
// ocorrência de negócio (não é falha do bot):
|
|
310
|
-
// ctx.items.occurrence('nota nº 10 não encontrada', { numero_nota: '10' }, { id: '10' })
|
|
311
|
-
});
|
|
312
|
-
```
|
|
78
|
+
| | `register(task, handler, options?)` | `start(task, handler, options?)` |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| Quem dispara | Uma **solicitação** no app (formulário com esse `task_slug`, ou agendamento) | O próprio robô, ao iniciar |
|
|
81
|
+
| `params` do handler | O JSON validado do formulário | `{}` |
|
|
82
|
+
| Quantas execuções | Uma por solicitação; `concurrency` em paralelo (1..5) | **Uma só** — o handler é o seu loop; quando ele retorna, a execução fecha |
|
|
83
|
+
| O processo | Fica vivo fazendo polling | Fica vivo pelo heartbeat, mesmo depois do handler retornar |
|
|
84
|
+
| Reexecutar pelo app | Sim | Não — só reiniciando o robô |
|
|
313
85
|
|
|
314
|
-
|
|
86
|
+
Os dois podem coexistir no mesmo processo: um monitor sempre ligado + tasks sob demanda, um heartbeat, uma instância. Cada slug só pode ser registrado uma vez.
|
|
315
87
|
|
|
316
|
-
|
|
88
|
+
> **Modo start e o limite de logs:** o servidor guarda no máximo **1000 linhas de log por execução**. Num loop que roda por dias, logue o que importa (ciclos com novidade, avisos, erros) e registre o trabalho em `hobots.items` — itens não têm esse teto tão baixo (10.000 por execução).
|
|
317
89
|
|
|
318
|
-
|
|
90
|
+
---
|
|
319
91
|
|
|
320
|
-
|
|
321
|
-
Hobots.register('baixar-notas', async (params, ctx) => {
|
|
322
|
-
const notas = await listarNotas(params);
|
|
323
|
-
ctx.progress.total(notas.length);
|
|
324
|
-
for (const nota of notas) {
|
|
325
|
-
await baixar(nota);
|
|
326
|
-
ctx.progress.advance();
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
```
|
|
92
|
+
## O objeto `hobots`
|
|
330
93
|
|
|
331
|
-
|
|
94
|
+
| Membro | O que faz |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| `hobots.log.debug/info/warn/error/success(msg, ...rest)` | Log da execução, enviado incrementalmente ao app (e ecoado no terminal). Aceita vários argumentos como `console.log` — `hobots.log.info('nota', numero, 'ok')` — cada um vira texto, unidos por espaço. **Nunca lança** — pode ir em `finally`. |
|
|
97
|
+
| `hobots.items.succeeded(payload?, { id }?)` · `.failed(msg, payload?, …)` · `.occurrence(msg, payload?, …)` · `.add({...})` | **A entrega do robô**: cada item processado, com status e atributos (viram colunas no app — declare-as com a opção `items: { columns: [...] }` do `register`/`start`). Bufferizado, em lotes. |
|
|
98
|
+
| `hobots.progress.total(n)` · `.advance(k = 1)` · `.set(atual, total?)` · `.current` · `.snapshot` | Barra, percentual e tempo restante na solicitação. Sem `total()` nada é enviado. |
|
|
99
|
+
| `hobots.transaction.startChild(op, descrição)` | Uma **step** no waterfall de performance da execução. A transaction da execução já existe e é finalizada pelo SDK. |
|
|
100
|
+
| `hobots.attach(imagem, { caption?, level? })` | Pendura um **print** (PNG/JPEG/WEBP, até 5 MB, 20 por execução) na linha de log. Nunca lança nem bloqueia. |
|
|
101
|
+
| `hobots.captureException(erro, hint?)` | Abre uma **issue** (módulo Issues). `hint.attachment` anexa um print à linha de log correspondente. |
|
|
102
|
+
| `hobots.retry(fn, { retries = 3, delayMs = 1_000, shouldRetry? })` | async-retry ciente do cancelamento da execução. `delayMs` aceita função `(tentativa) => ms`. |
|
|
103
|
+
| `hobots.sleep(ms)` | Espera **cancelável**: rejeita com `RunCanceled` se a execução for cancelada. |
|
|
104
|
+
| `hobots.signal` · `hobots.throwIfCanceled()` | Cancelamento cooperativo manual, para controle fino. |
|
|
105
|
+
| `hobots.runId` · `hobots.task` · `hobots.params` | Identidade da execução atual. |
|
|
106
|
+
| `hobots.register(...)` · `hobots.start(...)` · `hobots.close(timeoutMs?)` | Ciclo de vida do processo. |
|
|
107
|
+
|
|
108
|
+
O handler também recebe o mesmo contexto como 2º argumento (`(params, ctx) => …`), para quem prefere explícito. Fora de qualquer execução (ex.: antes do boot), os membros de contexto caem em fallbacks seguros e avisam uma vez — `hobots.log` continua imprimindo no terminal. Evite `const { log } = hobots` no topo de um módulo: o valor capturado é o de fora de execução; chame `hobots.log.info(...)`.
|
|
332
109
|
|
|
333
|
-
|
|
110
|
+
---
|
|
334
111
|
|
|
335
|
-
|
|
112
|
+
## Cancelamento automático
|
|
336
113
|
|
|
337
|
-
|
|
338
|
-
Hobots.register('processar-nfe', async (params, ctx) => {
|
|
339
|
-
await entrar();
|
|
340
|
-
Hobots.captureMessage('portal após o login', 'info', {
|
|
341
|
-
attachment: await page.screenshot(),
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
try {
|
|
345
|
-
await emitir(params);
|
|
346
|
-
} catch (erro) {
|
|
347
|
-
Hobots.captureException(erro, {
|
|
348
|
-
attachment: { image: await page.screenshot(), caption: 'tela no momento do erro' },
|
|
349
|
-
});
|
|
350
|
-
throw erro;
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
```
|
|
114
|
+
Uma execução cancelada no app **não mata** o handler — o SDK dispara `hobots.signal` e o próximo ponto de parada lança **`RunCanceled`**:
|
|
354
115
|
|
|
355
|
-
|
|
116
|
+
- `hobots.items.*`, `hobots.progress.total/advance/set`, `hobots.transaction.startChild()`, `hobots.sleep()` e cada tentativa de `hobots.retry()`.
|
|
117
|
+
- **Nunca `hobots.log.*`** — um log dentro de um `finally` de limpeza não pode explodir.
|
|
356
118
|
|
|
357
|
-
|
|
358
|
-
| --- | --- |
|
|
359
|
-
| formatos | PNG, JPEG, WEBP |
|
|
360
|
-
| tamanho por imagem | 5 MB |
|
|
361
|
-
| anexos por execução | 20 |
|
|
362
|
-
| anexos por linha de log | 4 |
|
|
119
|
+
Num loop de itens, isso costuma bastar: registrar o item processado já é o ponto de parada. Para controle fino, `hobots.throwIfCanceled()` e `hobots.signal` continuam disponíveis. Quando o cancelamento veio do app, o `RunCanceled` é **suprimido** — a execução já está `cancelada` no servidor e nada é reportado por cima. Limpeza (fechar navegador, desfazer estado) vai em `try/finally`.
|
|
363
120
|
|
|
364
|
-
|
|
121
|
+
> **Atenção:** não use `if (hobots.signal.aborted) return;`. Retornar normalmente reporta a execução como **`succeeded`**. É preciso **lançar** — e é o que os pontos de parada fazem.
|
|
365
122
|
|
|
366
|
-
|
|
123
|
+
---
|
|
367
124
|
|
|
368
|
-
|
|
125
|
+
## Retry
|
|
369
126
|
|
|
370
|
-
|
|
127
|
+
Quando o handler lança, o SDK **tenta de novo na mesma execução**: aviso no log (`tentativa 1/2 falhou: … — nova tentativa em 5000ms`), espera cancelável, re-invocação com os mesmos `params`. Default: **1 tentativa extra** com 5 s. Itens e progresso já enviados permanecem — escreva handlers **idempotentes** (use o `id` de negócio nos itens: repetido, o servidor ignora).
|
|
371
128
|
|
|
372
129
|
```js
|
|
373
|
-
|
|
374
|
-
for (const nota of params.notas) {
|
|
375
|
-
ctx.throwIfCanceled(); // lança RunCanceled se cancelada — o handler para aqui
|
|
376
|
-
await processar(nota);
|
|
377
|
-
}
|
|
378
|
-
});
|
|
130
|
+
hobots.register('processar-nfe', handler, { retries: 3, retryDelayMs: attempt => attempt * 10_000 });
|
|
379
131
|
```
|
|
380
132
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
> **Atenção:** não use `if (ctx.signal.aborted) return;`. Retornar normalmente faz o SDK reportar a run como **`succeeded`** — uma run cancelada apareceria como sucesso no app. É preciso **lançar**, e é o que o `throwIfCanceled()` faz.
|
|
384
|
-
|
|
385
|
-
### Concorrência
|
|
386
|
-
|
|
387
|
-
`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**: re-pesquisa imediatamente enquanto vem trabalho; quando não vem, espera `pollIntervalMs`.
|
|
133
|
+
Nunca há nova tentativa em **cancelamento**, **timeout** (`TaskTimeout` é terminal) ou execução já cancelada no app. Para uma chamada instável dentro do handler, use `hobots.retry()`.
|
|
388
134
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
`taskTimeoutMs` é **client-side**. No estouro o SDK **não mata** o handler — dispara `ctx.signal`, libera o slot e reporta a run como falha (`TaskTimeout`). Respeite o `signal` para parar de verdade. Sem ele, o backstop é o servidor.
|
|
392
|
-
|
|
393
|
-
### Idempotência (at-least-once até `running`)
|
|
135
|
+
---
|
|
394
136
|
|
|
395
|
-
|
|
137
|
+
## Opções
|
|
396
138
|
|
|
397
|
-
|
|
139
|
+
```ts
|
|
140
|
+
hobots.register(task, handler, options?: RegisterOptions);
|
|
141
|
+
hobots.start(task, handler, options?: StartOptions);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
| Opção | Default | Descrição |
|
|
145
|
+
| --- | --- | --- |
|
|
146
|
+
| `clientSecret` | env `HOBOTS_CLIENT_SECRET` | Client Secret do agente |
|
|
147
|
+
| `instanceId` | env `HOBOTS_INSTANCE_ID` → hostname | Identidade da instância |
|
|
148
|
+
| `environment` | env `HOBOTS_ENVIRONMENT` → `'production'` | `development` \| `testing` \| `staging` \| `production` — decide quem é alertado |
|
|
149
|
+
| `release` | — | Versão do programa, ex. `robo-nfe@1.4.2` |
|
|
150
|
+
| `tags` | — | Tags de todos os eventos |
|
|
151
|
+
| `debug` | `false` | Loga a atividade do SDK |
|
|
152
|
+
| `machineMetrics` | `true` | CPU/memória/disco no heartbeat e em cada evento |
|
|
153
|
+
| `captureUnhandled` | `true` | `uncaughtException`/`unhandledRejection` viram issues |
|
|
154
|
+
| `retries` | `1` | Tentativas extras do handler (0 desliga) |
|
|
155
|
+
| `retryDelayMs` | `5_000` | ms fixo ou `(tentativa) => ms` |
|
|
156
|
+
| `timeoutMs` | — (infinito) | Timeout por tentativa; terminal (`TaskTimeout`). O handler não é morto: o signal dispara e o slot é liberado |
|
|
157
|
+
| `concurrency` (register) | `1` | Execuções simultâneas desta task (1..5) |
|
|
158
|
+
| `pollIntervalMs` (register) | `2_000` | Intervalo entre polls sem trabalho (mín. 1_000) |
|
|
159
|
+
| avançadas: `installExitHandlers`, `logFlushIntervalMs`, `cancelCheckIntervalMs`, `cancelGraceMs` | `true`, `2_000`, `5_000`, `10_000` | Ver [Referência de API](./docs/api-reference.md) |
|
|
160
|
+
|
|
161
|
+
As opções de **processo** (`clientSecret`, `instanceId`, `environment`, `release`, `tags`, `debug`, `machineMetrics`, `captureUnhandled`, `installExitHandlers`, `pollIntervalMs`) valem as da **primeira** chamada; chamadas seguintes com valor diferente avisam e são ignoradas.
|
|
398
162
|
|
|
399
163
|
---
|
|
400
164
|
|
|
401
|
-
## Encerramento
|
|
165
|
+
## Encerramento
|
|
402
166
|
|
|
403
|
-
`close(timeoutMs
|
|
167
|
+
O heartbeat mantém o processo vivo por design. `hobots.close(timeoutMs = 30_000)` para o polling, aguarda as execuções em voo, para o heartbeat (libera o processo) e faz o flush final:
|
|
404
168
|
|
|
405
169
|
```js
|
|
406
|
-
// só telemetria: não segura o processo, mas faça o flush final para não perder eventos
|
|
407
|
-
await Hobots.close(2_000);
|
|
408
|
-
|
|
409
|
-
// com tasks: segura o processo por design — aguarde a run atual
|
|
410
170
|
process.on('SIGINT', () => {
|
|
411
|
-
void
|
|
171
|
+
void hobots.close(15_000).then(() => process.exit(0));
|
|
412
172
|
});
|
|
413
173
|
```
|
|
414
174
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
---
|
|
418
|
-
|
|
419
|
-
## Entry points / exports
|
|
175
|
+
Sem handler próprio de sinal, um `SIGINT`/`SIGTERM` (ou erro fatal) com execuções ativas reporta cada uma como `failed`/`AgentTerminated` antes do processo sair — o app mostra a falha na hora.
|
|
420
176
|
|
|
421
|
-
|
|
422
|
-
| --- | --- |
|
|
423
|
-
| `hobots` | Tudo: `init`, telemetria (captura, transactions, heartbeat), tasks (`register`/`start`/`stop`), `close`, classes e tipos. |
|
|
424
|
-
| `hobots/protocol` | Tipos do wire format (telemetria e tasks). |
|
|
425
|
-
|
|
426
|
-
Empacotado em **ESM e CommonJS** com **definições de tipos** (`.d.ts`) para todas as entradas.
|
|
177
|
+
> **Dados sensíveis:** logs, itens, mensagens de erro e prints são enviados ao servidor e ficam visíveis no app. O SDK redige padrões óbvios de segredo, mas não borra prints nem filtra dados pessoais. Capture só o necessário.
|
|
427
178
|
|
|
428
179
|
---
|
|
429
180
|
|
|
430
|
-
##
|
|
181
|
+
## Changelog resumido
|
|
431
182
|
|
|
432
|
-
|
|
183
|
+
- **3.1.0** — `hobots.log.*` aceita vários argumentos (como `console.log`); `retries` inválido (`NaN`) cai no default em vez de tentar para sempre; 429 de itens e 429/501/503 de anexo são definitivos (sem esperar a ladder — `quota`/`unsupported` passam a funcionar de fato); timeout **por requisição** nos anexos; truncamento nos limites do servidor (tag 512, fingerprint 10×255, nome de transaction 500, 200 steps, `exception.value` 8192, `filename` 1024); PNG acima de 20 000 px não declara dimensões; PATCH terminal nunca passa de 100 logs.
|
|
433
184
|
|
|
434
|
-
|
|
185
|
+
## Documentação
|
|
435
186
|
|
|
436
|
-
- [
|
|
437
|
-
- [
|
|
438
|
-
- [
|
|
439
|
-
- [
|
|
440
|
-
- [
|
|
441
|
-
-
|
|
442
|
-
|
|
443
|
-
---
|
|
187
|
+
- [Getting started](./docs/getting-started.md) — instalação, envs, primeira task nos dois modos.
|
|
188
|
+
- [Tasks](./docs/tasks.md) — handler, `hobots.*`, itens, progresso, anexos, cancelamento, retry, concorrência, idempotência.
|
|
189
|
+
- [Conceitos](./docs/concepts.md) — o vocabulário: task, execução, evento, transaction, step, heartbeat, instância, claim…
|
|
190
|
+
- [Referência de API](./docs/api-reference.md) — assinaturas, tabelas de opções e tempos.
|
|
191
|
+
- [Protocolo & transporte](./docs/protocol.md) — endpoints, envelope, retries, claim.
|
|
192
|
+
- Exemplos runnable em [`packages/hobots-sdk-demos`](../hobots-sdk-demos).
|
|
444
193
|
|
|
445
194
|
## Licença
|
|
446
195
|
|