hobots 2.0.1 → 2.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 CHANGED
@@ -261,6 +261,7 @@ interface RunContext {
261
261
  task: string;
262
262
  log: RunLogger; // ctx.log.debug/info/warn/error/success(message)
263
263
  items: RunItems; // ctx.items.succeeded/failed/occurrence — a entrega do bot
264
+ progress: RunProgressReporter; // ctx.progress.total/advance/set — barra + tempo restante no app
264
265
  tx: Transaction; // transaction desta run — correlacionada e finalizada pelo SDK
265
266
  signal: AbortSignal;
266
267
  throwIfCanceled(): void; // lança RunCanceled se o signal já disparou
@@ -271,6 +272,7 @@ interface RunContext {
271
272
  - **`params`** — os dados do formulário configurado no app (JSON validado). Tipável: `Hobots.register<{ lote: number }>(...)`.
272
273
  - **`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
274
  - **`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).
275
+ - **`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).
274
276
  - **`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).
275
277
  - **`ctx.signal`** — `AbortSignal` que dispara quando a run é **cancelada no app**, em **timeout** ou no **shutdown**.
276
278
  - **`ctx.throwIfCanceled()`** — o ponto de parada de uma linha; lança `RunCanceled` se o `signal` já disparou.
@@ -299,6 +301,23 @@ Hobots.register('baixar-notas', async (params, ctx) => {
299
301
 
300
302
  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
303
 
304
+ ### Progresso (`ctx.progress`)
305
+
306
+ Quando a run percorre uma lista conhecida, declare o total e avance a cada item — a solicitação no app ganha barra de progresso, percentual (`10/100 · 10%`) e **tempo restante estimado**:
307
+
308
+ ```js
309
+ Hobots.register('baixar-notas', async (params, ctx) => {
310
+ const notas = await listarNotas(params);
311
+ ctx.progress.total(notas.length);
312
+ for (const nota of notas) {
313
+ await baixar(nota);
314
+ ctx.progress.advance();
315
+ }
316
+ });
317
+ ```
318
+
319
+ Opcional e independente de `ctx.items` (use um, outro ou ambos). Sem `total()` nada é enviado. Chamadas síncronas e baratas: o snapshot pega carona nos PATCHes de log e, no pior caso, sai 1 vez por segundo; o PATCH terminal sempre leva o valor final. `ctx.progress.set(current, total?)` define um valor absoluto; `ctx.progress.current` e `ctx.progress.snapshot` leem o estado. Detalhes em [Progresso da execução](./docs/tasks.md#progresso-da-execução-ctxprogress).
320
+
302
321
  ### Anexos (screenshots)
303
322
 
304
323
  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 ? "2.0.1" : "0.0.0-dev";
164
+ var SDK_VERSION = true ? "2.2.0" : "0.0.0-dev";
165
165
  function isNode() {
166
166
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
167
167
  }
@@ -1201,6 +1201,7 @@ var MAX_ITEM_MESSAGE_CHARS = 2e3;
1201
1201
  var MAX_ITEM_PAYLOAD_BYTES = 16384;
1202
1202
  var ITEMS_FLUSH_AT_BUFFERED = 100;
1203
1203
  var ITEMS_DRAIN_MS = 15e3;
1204
+ var PROGRESS_FLUSH_MS = 1e3;
1204
1205
  function scrubPayloadValue(value, depth) {
1205
1206
  if (typeof value === "string") return scrubSecrets(value);
1206
1207
  if (depth >= 4 || value === null || typeof value !== "object") return value;
@@ -1235,12 +1236,28 @@ var RunReporter = class {
1235
1236
  failed: (message, payload, options) => this.addItem("failed", message, payload, options?.id),
1236
1237
  occurrence: (message, payload, options) => this.addItem("occurrence", message, payload, options?.id)
1237
1238
  };
1239
+ const progress = Object.defineProperties(
1240
+ {
1241
+ total: (total) => this.setProgress(this.progressCurrent, total),
1242
+ advance: (count = 1) => this.setProgress(this.progressCurrent + count, this.progressTotal),
1243
+ set: (current, total) => this.setProgress(current, total ?? this.progressTotal)
1244
+ },
1245
+ {
1246
+ current: { enumerable: true, get: () => this.progressCurrent },
1247
+ snapshot: {
1248
+ enumerable: true,
1249
+ get: () => this.progressTotal == null ? null : { current: this.progressCurrent, total: this.progressTotal }
1250
+ }
1251
+ }
1252
+ // defineProperties não tipa os getters adicionados — a interface é a fonte de verdade
1253
+ );
1238
1254
  const signal = this.abortController.signal;
1239
1255
  this.context = {
1240
1256
  runId,
1241
1257
  task,
1242
1258
  log,
1243
1259
  items,
1260
+ progress,
1244
1261
  tx,
1245
1262
  signal,
1246
1263
  throwIfCanceled: () => {
@@ -1284,6 +1301,14 @@ var RunReporter = class {
1284
1301
  uploadQueue = [];
1285
1302
  /** Uma vez que a instalação recusa anexos, para de tentar (e de avisar) nesta run. */
1286
1303
  attachmentsUnsupported = false;
1304
+ // Progresso (ctx.progress): snapshot absoluto, enviado com throttle via piggyback
1305
+ // em `enqueue()` — nunca gera tráfego próprio enquanto logs/heartbeat já fluem.
1306
+ progressCurrent = 0;
1307
+ progressTotal = null;
1308
+ /** Snapshot ainda não entregue ao servidor. */
1309
+ progressDirty = false;
1310
+ progressTimer;
1311
+ progressWarned = false;
1287
1312
  get isDead() {
1288
1313
  return this.dead;
1289
1314
  }
@@ -1301,6 +1326,7 @@ var RunReporter = class {
1301
1326
  this.abort();
1302
1327
  this.dead = true;
1303
1328
  this.clearFlushTimer();
1329
+ this.clearProgressTimer();
1304
1330
  this.buffer = [];
1305
1331
  this.itemsClosed = true;
1306
1332
  this.clearItemsTimer();
@@ -1380,13 +1406,63 @@ var RunReporter = class {
1380
1406
  reportSuccess() {
1381
1407
  this.stopCancelWatch();
1382
1408
  this.clearFlushTimer();
1409
+ this.forceFinalProgress();
1383
1410
  this.enqueue({ status: "succeeded", logs: this.takeLogs() });
1384
1411
  }
1385
1412
  reportFailure(error) {
1386
1413
  this.stopCancelWatch();
1387
1414
  this.clearFlushTimer();
1415
+ this.forceFinalProgress();
1388
1416
  this.enqueue({ status: "failed", error, logs: this.takeLogs() });
1389
1417
  }
1418
+ // ── Progresso ────────────────────────────────────────────────────────────────
1419
+ /** Valida, clampa e marca o snapshot como pendente; agenda flush só se nada mais vai sair. */
1420
+ setProgress(current, total) {
1421
+ if (this.dead) return;
1422
+ if (total != null && (!Number.isInteger(total) || total < 1)) {
1423
+ if (!this.progressWarned) {
1424
+ this.progressWarned = true;
1425
+ this.logger.warn(`ctx.progress: total inv\xE1lido (${String(total)}) \u2014 precisa ser inteiro >= 1; ignorado`);
1426
+ }
1427
+ return;
1428
+ }
1429
+ const safeCurrent = Number.isFinite(current) ? Math.max(0, Math.floor(current)) : this.progressCurrent;
1430
+ const next = total == null ? safeCurrent : Math.min(safeCurrent, total);
1431
+ if (next === this.progressCurrent && total === this.progressTotal) return;
1432
+ this.progressCurrent = next;
1433
+ this.progressTotal = total;
1434
+ if (total == null) return;
1435
+ this.progressDirty = true;
1436
+ if (this.progressTimer) return;
1437
+ this.progressTimer = setTimeout(() => {
1438
+ this.progressTimer = void 0;
1439
+ this.flushProgress();
1440
+ }, PROGRESS_FLUSH_MS);
1441
+ unrefTimer(this.progressTimer);
1442
+ }
1443
+ flushProgress() {
1444
+ this.clearProgressTimer();
1445
+ if (this.dead || !this.progressDirty) return;
1446
+ if (this.buffer.length > 0) this.flushLogs();
1447
+ else this.enqueue({ status: "running" });
1448
+ }
1449
+ clearProgressTimer() {
1450
+ if (this.progressTimer) {
1451
+ clearTimeout(this.progressTimer);
1452
+ this.progressTimer = void 0;
1453
+ }
1454
+ }
1455
+ /** O PATCH terminal sempre leva o snapshot final (o app mostra "87/100" numa run que falhou). */
1456
+ forceFinalProgress() {
1457
+ this.clearProgressTimer();
1458
+ this.progressDirty = this.progressTotal != null;
1459
+ }
1460
+ /** Consome o snapshot pendente para pegar carona no próximo PATCH. */
1461
+ takeProgress() {
1462
+ if (!this.progressDirty || this.progressTotal == null) return void 0;
1463
+ this.progressDirty = false;
1464
+ return { current: this.progressCurrent, total: this.progressTotal };
1465
+ }
1390
1466
  /**
1391
1467
  * Heartbeat de cancelamento: enquanto o handler roda, um PATCH 'running' vazio
1392
1468
  * periódico faz o servidor responder 409 se a run foi cancelada no app — o que
@@ -1649,11 +1725,14 @@ var RunReporter = class {
1649
1725
  new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
1650
1726
  ]);
1651
1727
  }
1652
- enqueue(update) {
1728
+ enqueue(updateIn) {
1653
1729
  if (this.dead) {
1654
- this.salvageLogs(update.logs);
1730
+ this.salvageLogs(updateIn.logs);
1655
1731
  return;
1656
1732
  }
1733
+ this.clearProgressTimer();
1734
+ const progress = this.takeProgress();
1735
+ const update = progress ? { ...updateIn, progress } : updateIn;
1657
1736
  this.pendingUpdates++;
1658
1737
  this.chain = this.chain.then(async () => {
1659
1738
  if (this.dead) {
@@ -1679,6 +1758,7 @@ var RunReporter = class {
1679
1758
  this.buffer.unshift(...update.logs);
1680
1759
  this.scheduleFlush();
1681
1760
  }
1761
+ if (update.progress && this.progressTotal != null) this.progressDirty = true;
1682
1762
  this.logger.error(`update da run ${this.runId} perdido:`, error);
1683
1763
  }
1684
1764
  }).finally(() => {