hobots 1.9.2 → 1.10.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 +21 -0
- package/dist/index.cjs +195 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -3
- package/dist/index.d.ts +33 -3
- package/dist/index.js +195 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +34 -1
- package/dist/protocol.d.ts +34 -1
- package/docs/api-reference.md +11 -0
- package/docs/protocol.md +35 -0
- package/docs/tasks.md +62 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -260,6 +260,7 @@ interface RunContext {
|
|
|
260
260
|
runId: string;
|
|
261
261
|
task: string;
|
|
262
262
|
log: RunLogger; // ctx.log.debug/info/warn/error/success(message)
|
|
263
|
+
items: RunItems; // ctx.items.succeeded/failed/occurrence — a entrega do bot
|
|
263
264
|
tx: Transaction; // transaction desta run — correlacionada e finalizada pelo SDK
|
|
264
265
|
signal: AbortSignal;
|
|
265
266
|
throwIfCanceled(): void; // lança RunCanceled se o signal já disparou
|
|
@@ -269,6 +270,7 @@ interface RunContext {
|
|
|
269
270
|
|
|
270
271
|
- **`params`** — os dados do formulário configurado no app (JSON validado). Tipável: `Hobots.register<{ lote: number }>(...)`.
|
|
271
272
|
- **`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.
|
|
273
|
+
- **`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).
|
|
272
274
|
- **`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).
|
|
273
275
|
- **`ctx.signal`** — `AbortSignal` que dispara quando a run é **cancelada no app**, em **timeout** ou no **shutdown**.
|
|
274
276
|
- **`ctx.throwIfCanceled()`** — o ponto de parada de uma linha; lança `RunCanceled` se o `signal` já disparou.
|
|
@@ -278,6 +280,25 @@ interface RunContext {
|
|
|
278
280
|
|
|
279
281
|
> **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.
|
|
280
282
|
|
|
283
|
+
### Itens de execução (`ctx.items`)
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
Hobots.register('baixar-notas', async (params, ctx) => {
|
|
287
|
+
for (const nota of await listarNotas(params)) {
|
|
288
|
+
try {
|
|
289
|
+
await baixar(nota);
|
|
290
|
+
ctx.items.succeeded({ numero_nota: nota.numero, cnpj_prestador: nota.cnpj }, { id: nota.numero });
|
|
291
|
+
} catch (erro) {
|
|
292
|
+
ctx.items.failed(String(erro), { numero_nota: nota.numero }, { id: nota.numero });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// ocorrência de negócio (não é falha do bot):
|
|
296
|
+
// ctx.items.occurrence('nota nº 10 não encontrada', { numero_nota: '10' }, { id: '10' })
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Chamadas síncronas e bufferizadas (lotes de até 500; dreno antes do reporte final). O `id` opcional é a identidade de negócio do item na run — repetido, é ignorado pelo servidor (registro único, retry idempotente). Limites: 10.000 itens/run, payload 16 KB, mensagem 2.000 caracteres. Detalhes em [Itens de execução](./docs/tasks.md#itens-de-execução-ctxitems).
|
|
301
|
+
|
|
281
302
|
### Anexos (screenshots)
|
|
282
303
|
|
|
283
304
|
Texto de log conta o que o robô fez; um print conta **como estava a tela**. Os métodos de captura (`Hobots.captureException` e `Hobots.captureMessage`) 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 — no app ela aparece como miniatura logo abaixo dessa linha, clicável para ampliar.
|
package/dist/index.cjs
CHANGED
|
@@ -161,7 +161,7 @@ function scrubSecrets(text) {
|
|
|
161
161
|
|
|
162
162
|
// src/utils.ts
|
|
163
163
|
var SDK_NAME = "hobots";
|
|
164
|
-
var SDK_VERSION = true ? "1.
|
|
164
|
+
var SDK_VERSION = true ? "1.10.0" : "0.0.0-dev";
|
|
165
165
|
function isNode() {
|
|
166
166
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
167
167
|
}
|
|
@@ -987,6 +987,38 @@ var TasksApi = class {
|
|
|
987
987
|
signal: AbortSignal.timeout(timeoutMs)
|
|
988
988
|
});
|
|
989
989
|
}
|
|
990
|
+
/**
|
|
991
|
+
* Envia um lote de itens de execução. Retry em falha de rede/5xx/429-transporte;
|
|
992
|
+
* 4xx é definitivo (404 = servidor sem o recurso, 429 = cap de itens por run) —
|
|
993
|
+
* o RunReporter trata por status.
|
|
994
|
+
*/
|
|
995
|
+
async createItems(runId, request) {
|
|
996
|
+
let response;
|
|
997
|
+
try {
|
|
998
|
+
response = await fetchWithRetry(
|
|
999
|
+
() => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/items`, {
|
|
1000
|
+
method: "POST",
|
|
1001
|
+
headers: this.headers(),
|
|
1002
|
+
body: JSON.stringify(request),
|
|
1003
|
+
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
1004
|
+
}),
|
|
1005
|
+
{ logger: this.logger, label: `itens da run ${runId}` }
|
|
1006
|
+
);
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
if (error instanceof Error && error.message === "HTTP 429") {
|
|
1009
|
+
throw new TasksApiError("envio de itens rejeitado (HTTP 429)", 429);
|
|
1010
|
+
}
|
|
1011
|
+
throw new Error(`envio de itens da run ${runId} descartado ap\xF3s retries: ${String(error)}`);
|
|
1012
|
+
}
|
|
1013
|
+
if (!response.ok) {
|
|
1014
|
+
throw new TasksApiError(`envio de itens rejeitado (HTTP ${response.status})`, response.status);
|
|
1015
|
+
}
|
|
1016
|
+
const body = await response.json().catch(() => null);
|
|
1017
|
+
return {
|
|
1018
|
+
accepted: typeof body?.accepted === "number" ? body.accepted : request.items.length,
|
|
1019
|
+
duplicates: typeof body?.duplicates === "number" ? body.duplicates : 0
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
990
1022
|
/**
|
|
991
1023
|
* Registra o anexo e obtém a URL presignada de upload. 404/501/503 significam
|
|
992
1024
|
* "esta instalação não tem anexos" — o RunReporter degrada em silêncio a partir daí.
|
|
@@ -1144,6 +1176,22 @@ var MAX_CANCEL_CHECK_INTERVAL_MS = 15e3;
|
|
|
1144
1176
|
var MAX_LOG_MESSAGE_CHARS = 1e4;
|
|
1145
1177
|
var MAX_LOGS_PER_PATCH = 100;
|
|
1146
1178
|
var FLUSH_AT_BUFFERED = 20;
|
|
1179
|
+
var MAX_ITEMS_PER_RUN = 1e4;
|
|
1180
|
+
var MAX_ITEMS_PER_REQUEST = 500;
|
|
1181
|
+
var MAX_ITEM_MESSAGE_CHARS = 2e3;
|
|
1182
|
+
var MAX_ITEM_PAYLOAD_BYTES = 16384;
|
|
1183
|
+
var ITEMS_FLUSH_AT_BUFFERED = 100;
|
|
1184
|
+
var ITEMS_DRAIN_MS = 15e3;
|
|
1185
|
+
function scrubPayloadValue(value, depth) {
|
|
1186
|
+
if (typeof value === "string") return scrubSecrets(value);
|
|
1187
|
+
if (depth >= 4 || value === null || typeof value !== "object") return value;
|
|
1188
|
+
if (Array.isArray(value)) return value.map((entry) => scrubPayloadValue(entry, depth + 1));
|
|
1189
|
+
const out = {};
|
|
1190
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1191
|
+
out[key] = scrubPayloadValue(entry, depth + 1);
|
|
1192
|
+
}
|
|
1193
|
+
return out;
|
|
1194
|
+
}
|
|
1147
1195
|
var RunReporter = class {
|
|
1148
1196
|
constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs) {
|
|
1149
1197
|
this.api = api;
|
|
@@ -1162,11 +1210,18 @@ var RunReporter = class {
|
|
|
1162
1210
|
success: (message) => this.log("success", message)
|
|
1163
1211
|
};
|
|
1164
1212
|
tx.setLifecycleSink((level, message) => this.push(level, message));
|
|
1213
|
+
const items = {
|
|
1214
|
+
add: (item) => this.addItem(item.status, item.message, item.payload, item.id),
|
|
1215
|
+
succeeded: (payload, options) => this.addItem("succeeded", void 0, payload, options?.id),
|
|
1216
|
+
failed: (message, payload, options) => this.addItem("failed", message, payload, options?.id),
|
|
1217
|
+
occurrence: (message, payload, options) => this.addItem("occurrence", message, payload, options?.id)
|
|
1218
|
+
};
|
|
1165
1219
|
const signal = this.abortController.signal;
|
|
1166
1220
|
this.context = {
|
|
1167
1221
|
runId,
|
|
1168
1222
|
task,
|
|
1169
1223
|
log,
|
|
1224
|
+
items,
|
|
1170
1225
|
tx,
|
|
1171
1226
|
signal,
|
|
1172
1227
|
throwIfCanceled: () => {
|
|
@@ -1185,6 +1240,23 @@ var RunReporter = class {
|
|
|
1185
1240
|
abortController = new AbortController();
|
|
1186
1241
|
flushIntervalMs;
|
|
1187
1242
|
cancelCheckIntervalMs;
|
|
1243
|
+
// Itens de execução vivem FORA de `this.chain` (status/liveness não pode ficar
|
|
1244
|
+
// atrás de milhares de itens), mas têm cadeia própria serializada — ordem de
|
|
1245
|
+
// envio preservada e sem lotes sobrepostos.
|
|
1246
|
+
itemsBuffer = [];
|
|
1247
|
+
itemsChain = Promise.resolve();
|
|
1248
|
+
itemsTimer;
|
|
1249
|
+
itemSeq = 0;
|
|
1250
|
+
itemCount = 0;
|
|
1251
|
+
/** Servidor sem o recurso (404) — para de tentar (e de avisar) nesta run. */
|
|
1252
|
+
itemsUnsupported = false;
|
|
1253
|
+
/** Cap de itens por run atingido (client-side ou 429) — degrada com um aviso só. */
|
|
1254
|
+
itemsQuotaExceeded = false;
|
|
1255
|
+
/**
|
|
1256
|
+
* Só o kill() do exit path fecha os itens — `dead` (run cancelada/finalizada no
|
|
1257
|
+
* servidor) NÃO fecha: o servidor aceita itens de run terminal (trabalho já feito).
|
|
1258
|
+
*/
|
|
1259
|
+
itemsClosed = false;
|
|
1188
1260
|
// Uploads de anexo vivem FORA de `this.chain`: a cadeia é um FIFO puro de
|
|
1189
1261
|
// status/logs e não pode ficar bloqueada esperando um binário subir.
|
|
1190
1262
|
pendingAttachments = /* @__PURE__ */ new Set();
|
|
@@ -1211,6 +1283,12 @@ var RunReporter = class {
|
|
|
1211
1283
|
this.dead = true;
|
|
1212
1284
|
this.clearFlushTimer();
|
|
1213
1285
|
this.buffer = [];
|
|
1286
|
+
this.itemsClosed = true;
|
|
1287
|
+
this.clearItemsTimer();
|
|
1288
|
+
if (this.itemsBuffer.length > 0) {
|
|
1289
|
+
this.logger.warn(`${this.itemsBuffer.length} item(ns) de execu\xE7\xE3o descartados (processo encerrando)`);
|
|
1290
|
+
this.itemsBuffer = [];
|
|
1291
|
+
}
|
|
1214
1292
|
const waiting = this.uploadQueue.splice(0, this.uploadQueue.length);
|
|
1215
1293
|
for (const resume of waiting) resume();
|
|
1216
1294
|
}
|
|
@@ -1313,6 +1391,120 @@ var RunReporter = class {
|
|
|
1313
1391
|
finalize() {
|
|
1314
1392
|
return this.chain;
|
|
1315
1393
|
}
|
|
1394
|
+
// ── Itens de execução ────────────────────────────────────────────────────────
|
|
1395
|
+
/**
|
|
1396
|
+
* Bufferiza um item (síncrono — a chamada nunca espera rede). Diferente do PATCH
|
|
1397
|
+
* de status, o servidor aceita itens mesmo com a run cancelada (trabalho já feito),
|
|
1398
|
+
* então um reporter morto não descarta: só um kill() de exit path descarta.
|
|
1399
|
+
*/
|
|
1400
|
+
addItem(status, message, payload, id) {
|
|
1401
|
+
if (this.itemsClosed || this.itemsUnsupported) return;
|
|
1402
|
+
if (this.itemCount >= MAX_ITEMS_PER_RUN) {
|
|
1403
|
+
if (!this.itemsQuotaExceeded) {
|
|
1404
|
+
this.itemsQuotaExceeded = true;
|
|
1405
|
+
this.log("warning", `itens descartados: limite de ${MAX_ITEMS_PER_RUN} por execu\xE7\xE3o atingido`);
|
|
1406
|
+
}
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
this.itemCount++;
|
|
1410
|
+
let scrubbedPayload;
|
|
1411
|
+
if (payload) {
|
|
1412
|
+
scrubbedPayload = scrubPayloadValue(payload, 0);
|
|
1413
|
+
let serializedLength;
|
|
1414
|
+
try {
|
|
1415
|
+
serializedLength = JSON.stringify(scrubbedPayload).length;
|
|
1416
|
+
} catch {
|
|
1417
|
+
}
|
|
1418
|
+
if (serializedLength === void 0 || serializedLength > MAX_ITEM_PAYLOAD_BYTES) {
|
|
1419
|
+
this.logger.warn(
|
|
1420
|
+
`payload do item descartado (${serializedLength === void 0 ? "n\xE3o serializ\xE1vel" : `${serializedLength} bytes > ${MAX_ITEM_PAYLOAD_BYTES}`}) \u2014 o item foi mantido`
|
|
1421
|
+
);
|
|
1422
|
+
scrubbedPayload = void 0;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
this.itemsBuffer.push({
|
|
1426
|
+
id: id ? truncate(String(id), 255) : uuid4(),
|
|
1427
|
+
seq: this.itemSeq++,
|
|
1428
|
+
status,
|
|
1429
|
+
...message ? { message: truncate(scrubSecrets(message), MAX_ITEM_MESSAGE_CHARS) } : {},
|
|
1430
|
+
...scrubbedPayload ? { payload: scrubbedPayload } : {},
|
|
1431
|
+
ts: nowISO()
|
|
1432
|
+
});
|
|
1433
|
+
if (this.itemsBuffer.length >= ITEMS_FLUSH_AT_BUFFERED) {
|
|
1434
|
+
this.flushItems();
|
|
1435
|
+
} else {
|
|
1436
|
+
this.scheduleItemsFlush();
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
scheduleItemsFlush() {
|
|
1440
|
+
if (this.itemsTimer) return;
|
|
1441
|
+
this.itemsTimer = setTimeout(() => {
|
|
1442
|
+
this.itemsTimer = void 0;
|
|
1443
|
+
this.flushItems();
|
|
1444
|
+
}, this.flushIntervalMs);
|
|
1445
|
+
unrefTimer(this.itemsTimer);
|
|
1446
|
+
}
|
|
1447
|
+
clearItemsTimer() {
|
|
1448
|
+
if (this.itemsTimer) {
|
|
1449
|
+
clearTimeout(this.itemsTimer);
|
|
1450
|
+
this.itemsTimer = void 0;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
/** Envia o buffer de itens em lotes serializados na cadeia própria. */
|
|
1454
|
+
flushItems() {
|
|
1455
|
+
this.clearItemsTimer();
|
|
1456
|
+
if (this.itemsBuffer.length === 0 || this.itemsUnsupported) return;
|
|
1457
|
+
const items = this.itemsBuffer.splice(0, this.itemsBuffer.length);
|
|
1458
|
+
for (let i = 0; i < items.length; i += MAX_ITEMS_PER_REQUEST) {
|
|
1459
|
+
const batch = items.slice(i, i + MAX_ITEMS_PER_REQUEST);
|
|
1460
|
+
this.itemsChain = this.itemsChain.then(() => this.sendItems(batch));
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
async sendItems(batch) {
|
|
1464
|
+
if (this.itemsUnsupported) return;
|
|
1465
|
+
try {
|
|
1466
|
+
await this.api.createItems(this.runId, { items: batch });
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
if (error instanceof TasksApiError && error.status === 404) {
|
|
1469
|
+
this.itemsUnsupported = true;
|
|
1470
|
+
this.logger.warn("itens de execu\xE7\xE3o n\xE3o s\xE3o suportados por este servidor Hobots \u2014 descartados");
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
if (error instanceof TasksApiError && error.status === 429) {
|
|
1474
|
+
if (!this.itemsQuotaExceeded) {
|
|
1475
|
+
this.itemsQuotaExceeded = true;
|
|
1476
|
+
this.log("warning", "itens descartados: limite de itens por execu\xE7\xE3o atingido no servidor");
|
|
1477
|
+
}
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
if (error instanceof TasksApiError) {
|
|
1481
|
+
this.logger.error(`lote de ${batch.length} item(ns) rejeitado pelo servidor:`, error);
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
this.itemsBuffer.unshift(...batch);
|
|
1485
|
+
this.scheduleItemsFlush();
|
|
1486
|
+
this.logger.error(`lote de ${batch.length} item(ns) n\xE3o enviado (devolvido ao buffer):`, error);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Dreno final de itens antes do PATCH terminal (com teto): garante que os
|
|
1491
|
+
* totalizadores estejam completos quando a run vira succeeded/failed. Como o
|
|
1492
|
+
* servidor aceita itens tardios, perder a corrida aqui não perde dados.
|
|
1493
|
+
*/
|
|
1494
|
+
async finalizeItems(timeoutMs = ITEMS_DRAIN_MS) {
|
|
1495
|
+
this.flushItems();
|
|
1496
|
+
await Promise.race([
|
|
1497
|
+
this.itemsChain,
|
|
1498
|
+
new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
|
|
1499
|
+
]);
|
|
1500
|
+
if (this.itemsBuffer.length > 0 && !this.itemsUnsupported && !this.itemsQuotaExceeded) {
|
|
1501
|
+
this.flushItems();
|
|
1502
|
+
await Promise.race([
|
|
1503
|
+
this.itemsChain,
|
|
1504
|
+
new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
|
|
1505
|
+
]);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1316
1508
|
// ── Anexos ───────────────────────────────────────────────────────────────────
|
|
1317
1509
|
/**
|
|
1318
1510
|
* Registra a linha de log com o id do anexo (síncrono, na ordem certa) e dispara
|
|
@@ -1757,12 +1949,14 @@ var TaskAgent = class {
|
|
|
1757
1949
|
() => this.invokeWithTimeout(handler, run, reporter)
|
|
1758
1950
|
);
|
|
1759
1951
|
await reporter.finalizeAttachments();
|
|
1952
|
+
await reporter.finalizeItems();
|
|
1760
1953
|
tx.setStatus("ok");
|
|
1761
1954
|
tx.finish();
|
|
1762
1955
|
reporter.reportSuccess();
|
|
1763
1956
|
this.logger.debug(`run ${run.id} conclu\xEDda`);
|
|
1764
1957
|
} catch (error) {
|
|
1765
1958
|
await reporter.finalizeAttachments();
|
|
1959
|
+
await reporter.finalizeItems();
|
|
1766
1960
|
tx.setStatus("error");
|
|
1767
1961
|
tx.finish();
|
|
1768
1962
|
reporter.reportFailure(toRunError(error));
|