hobots 2.3.0 → 3.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 +112 -369
- package/dist/index.cjs +1233 -852
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +223 -205
- package/dist/index.d.ts +223 -205
- package/dist/index.js +1233 -852
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +43 -4
- package/dist/protocol.d.ts +43 -4
- package/docs/README.md +16 -33
- package/docs/api-reference.md +74 -213
- package/docs/concepts.md +60 -389
- package/docs/getting-started.md +38 -84
- package/docs/protocol.md +29 -19
- package/docs/tasks.md +92 -251
- package/package.json +2 -2
- package/docs/telemetry.md +0 -162
package/docs/getting-started.md
CHANGED
|
@@ -6,116 +6,70 @@
|
|
|
6
6
|
pnpm add hobots
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
Sem dependências de runtime. Requer **Node 18+** (usa o `fetch` global).
|
|
9
|
+
Sem dependências de runtime. Requer **Node 18+** (usa o `fetch` global).
|
|
10
10
|
|
|
11
11
|
## Conceitos em 2 minutos
|
|
12
12
|
|
|
13
|
-
Os termos que aparecem nesta página, em uma frase cada. Explicação completa de todos em [Conceitos](./concepts.md).
|
|
14
|
-
|
|
15
13
|
| Conceito | Em uma frase |
|
|
16
14
|
| --- | --- |
|
|
17
|
-
| [
|
|
18
|
-
| [
|
|
19
|
-
| [
|
|
20
|
-
| [
|
|
21
|
-
| [
|
|
22
|
-
| [
|
|
23
|
-
|
|
24
|
-
## O Client Secret
|
|
25
|
-
|
|
26
|
-
Cada agente tem **um único Client Secret** — a mesma credencial autentica a telemetria **e** as tasks:
|
|
27
|
-
|
|
28
|
-
```
|
|
29
|
-
hb_<64 hex>
|
|
30
|
-
```
|
|
15
|
+
| [Task](./concepts.md#task) | O que o robô sabe fazer, identificado por um slug — registrado com `register` (sob demanda) ou `start` (sempre ligado) |
|
|
16
|
+
| [Execução (run)](./concepts.md#execução-run) | Uma rodada de uma task, com logs, itens, progresso e resultado — uma solicitação no app |
|
|
17
|
+
| [Item](./concepts.md#item) | Uma unidade de trabalho entregue pela execução (nota baixada, boleto emitido) |
|
|
18
|
+
| [Heartbeat](./concepts.md#heartbeat) | Batimento "estou vivo" — sempre ligado; o servidor acusa offline quando para |
|
|
19
|
+
| [instanceId](./concepts.md#instanceid) | A identidade da instância do robô no painel |
|
|
20
|
+
| [Issue](./concepts.md#evento-e-issue) | Erros agrupados — `hobots.captureException` e erros não tratados |
|
|
31
21
|
|
|
32
|
-
|
|
22
|
+
## Credenciais
|
|
33
23
|
|
|
34
|
-
|
|
24
|
+
Cada agente tem **um Client Secret** (`hb_<64 hex>`), copiado em **Configurações → Agentes**. O SDK lê do ambiente, como as libs da AWS:
|
|
35
25
|
|
|
36
26
|
```bash
|
|
37
|
-
|
|
27
|
+
export HOBOTS_CLIENT_SECRET=hb_<64 hex>
|
|
28
|
+
export HOBOTS_INSTANCE_ID=vm-cliente-01 # opcional — sem ela, o hostname
|
|
38
29
|
```
|
|
39
30
|
|
|
40
|
-
|
|
31
|
+
Também dá para passar `clientSecret`/`instanceId` nas opções da task (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`.
|
|
41
32
|
|
|
42
|
-
##
|
|
33
|
+
## Primeira task sob demanda (`register`)
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
- Quer só saber se o robô quebrou, quanto demora e se está vivo? → `init({ clientSecret, heartbeat: true })`.
|
|
47
|
-
- Quer disparar o robô sob demanda a partir de um formulário no app? → `init({ clientSecret, tasks: true })` + `register()` + `start()`.
|
|
48
|
-
- Ambos? Passe `heartbeat` e `tasks` no **mesmo `init()`**. É o cenário do
|
|
49
|
-
[`full.ts`](../../hobots-sdk-demos/src/full.ts) nos demos do SDK.
|
|
50
|
-
|
|
51
|
-
## O `init` único
|
|
35
|
+
1. No app, crie um formulário em **Configurações → Formulários** com o `task_slug` `processar-nfe`.
|
|
36
|
+
2. No robô:
|
|
52
37
|
|
|
53
38
|
```js
|
|
54
|
-
import
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
39
|
+
import hobots from 'hobots';
|
|
40
|
+
|
|
41
|
+
hobots.register('processar-nfe', async params => {
|
|
42
|
+
hobots.log.info(`lote ${params.lote}`);
|
|
43
|
+
for (const nota of params.notas) {
|
|
44
|
+
await baixar(nota);
|
|
45
|
+
hobots.items.succeeded({ numero_nota: nota }, { id: nota });
|
|
46
|
+
}
|
|
47
|
+
hobots.log.success('concluído');
|
|
62
48
|
});
|
|
63
49
|
|
|
64
|
-
|
|
65
|
-
// Captura manual:
|
|
66
|
-
try {
|
|
67
|
-
await processarNota(nota);
|
|
68
|
-
} catch (error) {
|
|
69
|
-
Hobots.captureException(error, { tags: { cliente: 'acme' } });
|
|
70
|
-
throw error;
|
|
71
|
-
}
|
|
50
|
+
process.on('SIGINT', () => void hobots.close(15_000).then(() => process.exit(0)));
|
|
72
51
|
```
|
|
73
52
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
## Primeiro agente (tasks)
|
|
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 **Solicitações** com logs em tempo real.
|
|
77
54
|
|
|
78
|
-
|
|
55
|
+
## Primeira task sempre ligada (`start`)
|
|
79
56
|
|
|
80
57
|
```js
|
|
81
|
-
import
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
Hobots.register('processar-nfe', async (params, ctx) => {
|
|
90
|
-
ctx.log.info(`processando lote ${params.lote}…`);
|
|
91
|
-
// Hobots.captureMessage('portal após o login', 'info', { attachment: await page.screenshot() }); // print junto do log
|
|
92
|
-
ctx.log.info('12 notas processadas'); // o retorno do handler é ignorado — reporte por log
|
|
58
|
+
import hobots from 'hobots';
|
|
59
|
+
|
|
60
|
+
hobots.start('monitor-pasta', async () => {
|
|
61
|
+
hobots.log.info('monitor iniciado');
|
|
62
|
+
while (true) {
|
|
63
|
+
await hobots.sleep(60_000); // cancelável — para se a execução for cancelada no app
|
|
64
|
+
for (const arquivo of await novosArquivos()) hobots.items.succeeded({ arquivo });
|
|
65
|
+
}
|
|
93
66
|
});
|
|
94
|
-
Hobots.start(); // mantém o processo vivo até stop()/close()
|
|
95
67
|
```
|
|
96
68
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
As execuções aparecem no módulo **Solicitações** do app; o `task` do `register()` é o `task_slug` do formulário em **Configurações → Formulários**. Detalhes (cancelamento, concorrência, idempotência) em [Tasks](./tasks.md).
|
|
69
|
+
O robô abre a própria execução ao iniciar e ela aparece em **Solicitações** com origem "agente". Quando o handler retorna (ou é cancelado), a execução fecha; o processo continua vivo pelo heartbeat até `hobots.close()`.
|
|
100
70
|
|
|
101
71
|
## Encerramento
|
|
102
72
|
|
|
103
|
-
`close()`
|
|
104
|
-
|
|
105
|
-
Sem `start()` chamado, o SDK não segura o processo (timers `unref`'d), mas faça o flush final para não perder eventos em buffer:
|
|
106
|
-
|
|
107
|
-
```js
|
|
108
|
-
await Hobots.close(2000);
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Com as tasks, `start()` segura o processo por design. Encerre de forma graciosa aguardando a run atual:
|
|
112
|
-
|
|
113
|
-
```js
|
|
114
|
-
process.on('SIGINT', () => {
|
|
115
|
-
void Hobots.close(15_000).then(() => process.exit(0));
|
|
116
|
-
});
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
> **Atenção:** o `timeoutMs` do `close()` é gasto **duas vezes, em sequência** — uma esperando as runs de tasks, outra no flush final. O `close(15_000)` acima pode levar até ~30s no pior caso. Se o seu SIGTERM tem prazo curto (Kubernetes, systemd), dimensione por aí. Os defaults, quando você omite o argumento, estão em [Tempos e intervalos](./api-reference.md#tempos-e-intervalos).
|
|
73
|
+
`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.
|
|
120
74
|
|
|
121
|
-
|
|
75
|
+
Próximos passos: [Tasks](./tasks.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
|
@@ -18,7 +18,7 @@ hb_<64 hex>
|
|
|
18
18
|
| Tasks (execução) | `<baseUrl>/v1/tasks/*` | `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
|
|
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 na primeira task, 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
|
|
|
@@ -38,11 +38,11 @@ Endpoint fixo `POST /v1/ingest` recebe **envelopes** JSON autenticados por Beare
|
|
|
38
38
|
|
|
39
39
|
Tipos de item: `event`, `transaction`, `heartbeat` (definidos em `hobots/protocol`).
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Todo `transaction` carrega `run_id`: é assim que a [transaction da execução](./concepts.md#transaction) (`hobots.transaction`) é correlacionada, e o que permite o app montar o waterfall de performance dentro da solicitação.
|
|
42
42
|
|
|
43
43
|
`items` aceita de 1 a **50** itens por envelope — acima disso a API responde `422`. O `maxBatchSize` é só o gatilho do flush: quando a fila tem mais que esse teto (burst de captures, ou fila acumulada durante uma janela de rede ruim), o transport a fatia em lotes de 50 e faz um POST por lote.
|
|
44
44
|
|
|
45
|
-
**Flush & retries:** o transport flusha
|
|
45
|
+
**Flush & retries:** o transport flusha a cada 2 s ou ao acumular 10 itens. Ladder de retry de 3 tentativas; respostas 4xx (exceto 429) são descartadas sem retry; a fila tem teto fixo e descarta os itens **mais antigos** ao encher. Todos os valores em [Tempos e intervalos](./api-reference.md#tempos-e-intervalos).
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
### MachineSnapshot
|
|
@@ -52,7 +52,7 @@ Recursos da máquina onde a instância roda. Vai em `HeartbeatPayload.machine`,
|
|
|
52
52
|
| Campo | Tipo | Descrição |
|
|
53
53
|
| --- | --- | --- |
|
|
54
54
|
| `sampled_at` | ISO 8601 | momento da coleta (relógio do cliente) — chave de dedupe da série |
|
|
55
|
-
| `instance_id` | string | o `instanceId`
|
|
55
|
+
| `instance_id` | string | o `instanceId` da instância — liga o snapshot de um evento à série da instância |
|
|
56
56
|
| `hostname`, `os`, `arch` | string | identificação da máquina (`process.platform` / `sys.platform`) |
|
|
57
57
|
| `cpu_count` | int | cores lógicos |
|
|
58
58
|
| `cpu_percent` | number \| null | uso de CPU do sistema — média entre duas coletas; `null` até haver 2 amostras |
|
|
@@ -66,9 +66,9 @@ 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
|
-
## Tasks — polling e reporte
|
|
69
|
+
## Tasks — polling, criação e reporte
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
Endpoints sob `<baseUrl>/v1/tasks`, todos com `Authorization: Bearer <clientSecret>` (a credencial única do agente):
|
|
72
72
|
|
|
73
73
|
### `POST /v1/tasks/poll`
|
|
74
74
|
|
|
@@ -79,21 +79,31 @@ Corpo `PollRequest`:
|
|
|
79
79
|
"instance": {
|
|
80
80
|
"instance_id": "vm-01", // identidade canônica da instância (obrigatória)
|
|
81
81
|
"name": "VM 01",
|
|
82
|
-
"version": "
|
|
82
|
+
"version": "3.0.0",
|
|
83
83
|
"host": "vm-01",
|
|
84
|
-
"
|
|
84
|
+
"environment": "production", // ambiente da instância: development | testing | staging | production
|
|
85
|
+
"tasks": ["processar-nfe", "monitor-pasta"], // TODAS as tasks (register + start) — o painel mostra o que o agente sabe fazer
|
|
86
|
+
"poll_interval_ms": 2000
|
|
85
87
|
},
|
|
86
|
-
"max":
|
|
87
|
-
"tasks": ["processar-nfe"] // só
|
|
88
|
+
"max": 2, // soma dos slots livres das tasks remotas (1..5)
|
|
89
|
+
"tasks": ["processar-nfe"] // só tasks de register() com slot livre — a concorrência é por task
|
|
88
90
|
}
|
|
89
91
|
```
|
|
90
92
|
|
|
91
93
|
Resposta `PollResponse` = `{ runs: RunDescriptor[] }`, cada `{ id, task, params, form, source, created_at }`.
|
|
92
94
|
|
|
93
|
-
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)`.
|
|
95
|
+
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 só com tasks de `start()` nunca faz poll.
|
|
94
96
|
|
|
95
97
|
**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]`.
|
|
96
98
|
|
|
99
|
+
### `POST /v1/tasks/runs`
|
|
100
|
+
|
|
101
|
+
Modo `start`: a instância abre a **própria run**. Corpo `RunCreateRequest` — `{ "task": "monitor-pasta", "instance": { /* InstanceInfo, obrigatório */ } }`. Resposta `201` `RunCreateResponse` = o mesmo `RunDescriptor` do poll, com `params: {}`, `form: null` e `source: 'agent'`.
|
|
102
|
+
|
|
103
|
+
- 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.
|
|
104
|
+
- `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, sem `instance.instance_id` ou com `task` fora de 1..255 → `422`.
|
|
105
|
+
- 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.
|
|
106
|
+
|
|
97
107
|
### `PATCH /v1/tasks/runs/{id}`
|
|
98
108
|
|
|
99
109
|
Corpo `RunUpdateRequest`:
|
|
@@ -107,23 +117,23 @@ Corpo `RunUpdateRequest`:
|
|
|
107
117
|
"attachments": ["<32 hex>"] } // opcional; ids de anexo (máx. 4)
|
|
108
118
|
],
|
|
109
119
|
"error": { "type": "Error", "message": "…", "stack": "…" }, // só com 'failed'
|
|
110
|
-
"progress": { "current": 10, "total": 100 } // opcional
|
|
120
|
+
"progress": { "current": 10, "total": 100 } // opcional: snapshot de hobots.progress
|
|
111
121
|
}
|
|
112
122
|
```
|
|
113
123
|
|
|
114
|
-
- `status: 'running'` pode repetir para enviar **logs incrementais** (throttle por `logFlushIntervalMs`, flush ao acumular 20, chunk de 100, teto de 1000 logs/run, mensagem cap 10k).
|
|
115
|
-
- `progress` é **opcional** e aditivo
|
|
124
|
+
- `status: 'running'` pode repetir para enviar **logs incrementais** (throttle por `logFlushIntervalMs`, flush ao acumular 20, chunk de 100, teto de 1000 logs/run, mensagem cap 10k). O aviso de cada tentativa do retry do handler é uma dessas linhas.
|
|
125
|
+
- `progress` é **opcional** e aditivo: snapshot absoluto de `hobots.progress` (`current` 0..total, `total` ≥ 1) — o servidor **sobrescreve** `progress_current`/`progress_total` e grava `progress_started_at` no primeiro PATCH que o traga (relógio do servidor; âncora da ETA no app). `current > total` é clampado; progresso enviado a uma run já terminal é descartado. O SDK anexa o snapshot mais recente a **qualquer** PATCH que saia (logs, heartbeat, terminal) e só dispara um PATCH próprio, no máximo 1/s, quando nada mais está a caminho. Uma API anterior ao recurso descarta o campo (objeto não-strict).
|
|
116
126
|
- `attachments` é **opcional** e aditivo: SDKs anteriores à 1.9 simplesmente não enviam o campo, e uma API anterior ao recurso descarta o campo na validação (objeto não-strict) sem rejeitar o PATCH. Os ids são gerados pelo agente **antes** do upload, o que permite enviar a linha de log sem esperar o binário subir.
|
|
117
127
|
- Ordem garantida `running → logs → terminal` (PATCHes serializados numa cadeia de promise).
|
|
118
|
-
- Retry `[1s, 4s, 16s]`; 4xx exceto 429 é terminal (**409** = run cancelada / transição inválida → marca a run como morta e dispara `
|
|
119
|
-
- Todo PATCH atualiza `last_report_at` no servidor (relógio do servidor) — é o **sinal de vida da run**. O heartbeat de cancelamento (PATCH `running` vazio a cada `cancelCheckIntervalMs`, máx 15s) garante reporte contínuo; um sweep server-side marca como `failed` com erro `AgentTerminated` ("execução encerrada inesperadamente") toda run `running` sem reporte há **
|
|
128
|
+
- Retry `[1s, 4s, 16s]`; 4xx exceto 429 é terminal (**409** = run cancelada / transição inválida → marca a run como morta e dispara `hobots.signal`).
|
|
129
|
+
- Todo PATCH atualiza `last_report_at` no servidor (relógio do servidor) — é o **sinal de vida da run**. O heartbeat de cancelamento (PATCH `running` vazio a cada `cancelCheckIntervalMs`, máx 15s) garante reporte contínuo; um sweep server-side marca como `failed` com erro `AgentTerminated` ("execução encerrada inesperadamente") toda run `running` sem reporte há **120s**, sem re-entrega. No exit path (sinal/erro fatal com runs ativas), o SDK envia um único PATCH `failed`/`AgentTerminated` com timeout curto (~2s), sem retry — o sweep é o backstop.
|
|
120
130
|
- O protocolo **não transporta resultado** de execução: o retorno do handler é ignorado pelo SDK e o servidor descarta qualquer campo `result` (dados de negócio podem ser sensíveis e não são coletados).
|
|
121
131
|
|
|
122
|
-
Ciclo de vida completo da run no servidor (`RunStatus`): `pending → claimed → running → succeeded | failed | canceled
|
|
132
|
+
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`).
|
|
123
133
|
|
|
124
134
|
### `POST /v1/tasks/runs/{id}/items`
|
|
125
135
|
|
|
126
|
-
Registra em lote os **itens de execução** (a entrega do bot — `
|
|
136
|
+
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).
|
|
127
137
|
|
|
128
138
|
Corpo `RunItemsCreateRequest`:
|
|
129
139
|
|
|
@@ -212,4 +222,4 @@ O `POST /v1/tasks/poll` no app chama `claimPendingRuns()` em
|
|
|
212
222
|
|
|
213
223
|
## Identidade do SDK
|
|
214
224
|
|
|
215
|
-
`SDK_NAME = 'hobots'` e `SDK_VERSION` ficam em `src/utils.ts` e vão em todo envelope (campo `sdk`) e no `
|
|
225
|
+
`SDK_NAME = 'hobots'` e `SDK_VERSION` ficam em `src/utils.ts` e vão em todo envelope (campo `sdk`) e no `instance.version` do poll e da criação de run. A `SDK_VERSION` é **injetada pelo build** (o `define` do tsup lê a `version` do `package.json`) — não há sincronização manual ao publicar.
|